feat: use commas to select a tag in multi-select grid cells (#1158)

* fix: comma to select tags

* chore: support passing multiple option ids

* chore: add more unit tests for single select and multi select

* chore: move to select multiple options using a single payload

* chore: do not unselect the option if insert option ids contain that option

Co-authored-by: appflowy <annie@appflowy.io>
This commit is contained in:
Richard Shiue 2022-10-11 13:43:29 +08:00 committed by GitHub
parent d80a67bdda
commit 295b887cf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 382 additions and 213 deletions

View File

@ -57,6 +57,10 @@ class SelectOptionCellEditorBloc
trySelectOption: (_TrySelectOption value) {
_trySelectOption(value.optionName, emit);
},
selectMultipleOptions: (_SelectMultipleOptions value) {
_selectMultipleOptions(value.optionNames);
_filterOption(value.remainder, emit);
},
filterOption: (_SelectOptionFilter value) {
_filterOption(value.optionName, emit);
},
@ -97,14 +101,14 @@ class SelectOptionCellEditorBloc
final hasSelected = state.selectedOptions
.firstWhereOrNull((option) => option.id == optionId);
if (hasSelected != null) {
_selectOptionService.unSelect(optionId: optionId);
_selectOptionService.unSelect(optionIds: [optionId]);
} else {
_selectOptionService.select(optionId: optionId);
_selectOptionService.select(optionIds: [optionId]);
}
}
void _trySelectOption(
String optionName, Emitter<SelectOptionEditorState> emit) async {
String optionName, Emitter<SelectOptionEditorState> emit) {
SelectOptionPB? matchingOption;
bool optionExistsButSelected = false;
@ -126,13 +130,20 @@ class SelectOptionCellEditorBloc
// if there is an unselected matching option, select it
if (matchingOption != null) {
_selectOptionService.select(optionId: matchingOption.id);
_selectOptionService.select(optionIds: [matchingOption.id]);
}
// clear the filter
emit(state.copyWith(filter: none()));
}
void _selectMultipleOptions(List<String> optionNames) {
final optionIds = state.options
.where((e) => optionNames.contains(e.name))
.map((e) => e.id);
_selectOptionService.select(optionIds: optionIds);
}
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
final _MakeOptionResult result =
_makeOptions(Some(optionName), state.allOptions);
@ -222,6 +233,8 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
_SelectOptionFilter;
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
_TrySelectOption;
const factory SelectOptionEditorEvent.selectMultipleOptions(
List<String> optionNames, String remainder) = _SelectMultipleOptions;
}
@freezed

View File

@ -26,7 +26,7 @@ class SelectOptionService {
..fieldId = fieldId
..rowId = rowId;
final payload = SelectOptionChangesetPayloadPB.create()
..insertOption = option
..insertOptions.add(option)
..cellIdentifier = cellIdentifier;
return GridEventUpdateSelectOption(payload).send();
},
@ -40,7 +40,7 @@ class SelectOptionService {
required SelectOptionPB option,
}) {
final payload = SelectOptionChangesetPayloadPB.create()
..updateOption = option
..updateOptions.add(option)
..cellIdentifier = _cellIdentifier();
return GridEventUpdateSelectOption(payload).send();
}
@ -49,7 +49,7 @@ class SelectOptionService {
required SelectOptionPB option,
}) {
final payload = SelectOptionChangesetPayloadPB.create()
..deleteOption = option
..deleteOptions.add(option)
..cellIdentifier = _cellIdentifier();
return GridEventUpdateSelectOption(payload).send();
@ -64,17 +64,19 @@ class SelectOptionService {
return GridEventGetSelectOptionCellData(payload).send();
}
Future<Either<void, FlowyError>> select({required String optionId}) {
Future<Either<void, FlowyError>> select(
{required Iterable<String> optionIds}) {
final payload = SelectOptionCellChangesetPayloadPB.create()
..cellIdentifier = _cellIdentifier()
..insertOptionId = optionId;
..insertOptionIds.addAll(optionIds);
return GridEventUpdateSelectOptionCell(payload).send();
}
Future<Either<void, FlowyError>> unSelect({required String optionId}) {
Future<Either<void, FlowyError>> unSelect(
{required Iterable<String> optionIds}) {
final payload = SelectOptionCellChangesetPayloadPB.create()
..cellIdentifier = _cellIdentifier()
..deleteOptionId = optionId;
..deleteOptionIds.addAll(optionIds);
return GridEventUpdateSelectOptionCell(payload).send();
}

View File

@ -149,6 +149,7 @@ class _TextField extends StatelessWidget {
distanceToText: _editorPanelWidth * 0.7,
maxLength: 30,
tagController: _tagController,
textSeparators: const [','],
onClick: () => popoverMutex.close(),
newText: (text) {
context
@ -160,6 +161,14 @@ class _TextField extends StatelessWidget {
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.trySelectOption(tagName));
},
onPaste: (tagNames, remainder) {
context
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.selectMultipleOptions(
tagNames,
remainder,
));
},
),
);
},

View File

@ -17,9 +17,11 @@ class SelectOptionTextField extends StatefulWidget {
final List<SelectOptionPB> options;
final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
final double distanceToText;
final List<String> textSeparators;
final Function(String) onSubmitted;
final Function(String) newText;
final Function(List<String>, String) onPaste;
final VoidCallback? onClick;
final int? maxLength;
@ -29,7 +31,9 @@ class SelectOptionTextField extends StatefulWidget {
required this.distanceToText,
required this.tagController,
required this.onSubmitted,
required this.onPaste,
required this.newText,
required this.textSeparators,
this.onClick,
this.maxLength,
TextEditingController? textController,
@ -65,7 +69,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
textfieldTagsController: widget.tagController,
initialTags: widget.selectedOptionMap.keys.toList(),
focusNode: focusNode,
textSeparators: const [','],
textSeparators: widget.textSeparators,
inputfieldBuilder: (
BuildContext context,
editController,
@ -83,7 +87,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
if (onChanged != null) {
onChanged(text);
}
widget.newText(text);
_newText(text, editController);
},
onSubmitted: (text) {
if (onSubmitted != null) {
@ -121,6 +125,40 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
);
}
void _newText(String text, TextEditingController editingController) {
if (text.isEmpty) {
widget.newText('');
return;
}
final trimmedText = text.trim();
List<String> splits = [];
String currentString = '';
// split the string into tokens
for (final char in trimmedText.split('')) {
if (!widget.textSeparators.contains(char)) {
currentString += char;
continue;
}
if (currentString.isNotEmpty) {
splits.add(currentString);
}
currentString = '';
}
// add the remainder (might be '')
splits.add(currentString);
final submittedOptions =
splits.sublist(0, splits.length - 1).map((e) => e.trim()).toList();
final remainder = splits.elementAt(splits.length - 1).trimLeft();
editingController.text = remainder;
editingController.selection =
TextSelection.collapsed(offset: controller.text.length);
widget.onPaste(submittedOptions, remainder);
}
Widget? _renderTags(BuildContext context, ScrollController sc) {
if (widget.selectedOptionMap.isEmpty) {
return null;

View File

@ -341,19 +341,19 @@ pub(crate) async fn update_select_option_handler(
let mut cell_content_changeset = None;
let mut is_changed = None;
if let Some(option) = changeset.insert_option {
cell_content_changeset = Some(SelectOptionCellChangeset::from_insert(&option.id).to_str());
for option in changeset.insert_options {
cell_content_changeset = Some(SelectOptionCellChangeset::from_insert_option_id(&option.id).to_str());
type_option.insert_option(option);
is_changed = Some(());
}
if let Some(option) = changeset.update_option {
for option in changeset.update_options {
type_option.insert_option(option);
is_changed = Some(());
}
if let Some(option) = changeset.delete_option {
cell_content_changeset = Some(SelectOptionCellChangeset::from_delete(&option.id).to_str());
for option in changeset.delete_options {
cell_content_changeset = Some(SelectOptionCellChangeset::from_delete_option_id(&option.id).to_str());
type_option.delete_option(option);
is_changed = Some(());
}

View File

@ -244,14 +244,14 @@ pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevisi
CellRevision::new(data)
}
pub fn insert_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision {
let cell_data = SelectOptionCellChangeset::from_insert(&option_id).to_str();
pub fn insert_select_option_cell(option_ids: Vec<String>, field_rev: &FieldRevision) -> CellRevision {
let cell_data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
CellRevision::new(data)
}
pub fn delete_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision {
let cell_data = SelectOptionCellChangeset::from_delete(&option_id).to_str();
pub fn delete_select_option_cell(option_ids: Vec<String>, field_rev: &FieldRevision) -> CellRevision {
let cell_data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap();
CellRevision::new(data)
}

View File

@ -4,7 +4,7 @@ use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOper
use crate::services::field::type_options::util::get_cell_data;
use crate::services::field::{
make_selected_select_options, BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionCellDataPB,
SelectOptionIds, SelectOptionOperation, SelectOptionPB, TypeOptionBuilder, SELECTION_IDS_SEPARATOR,
SelectOptionIds, SelectOptionOperation, SelectOptionPB, TypeOptionBuilder,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
@ -61,29 +61,32 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for MultiSele
cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let content_changeset = changeset.try_into_inner()?;
let insert_option_ids = content_changeset
.insert_option_ids
.into_iter()
.filter(|insert_option_id| self.options.iter().any(|option| &option.id == insert_option_id))
.collect::<Vec<String>>();
let new_cell_data: String;
match cell_rev {
None => {
new_cell_data = content_changeset.insert_option_id.unwrap_or_else(|| "".to_owned());
new_cell_data = SelectOptionIds::from(insert_option_ids).to_string();
}
Some(cell_rev) => {
let cell_data = get_cell_data(&cell_rev);
let mut select_ids: SelectOptionIds = cell_data.into();
if let Some(insert_option_id) = content_changeset.insert_option_id {
tracing::trace!("Insert multi select option: {}", &insert_option_id);
if select_ids.contains(&insert_option_id) {
select_ids.retain(|id| id != &insert_option_id);
} else {
for insert_option_id in insert_option_ids {
if !select_ids.contains(&insert_option_id) {
select_ids.push(insert_option_id);
}
}
if let Some(delete_option_id) = content_changeset.delete_option_id {
tracing::trace!("Delete multi select option: {}", &delete_option_id);
for delete_option_id in content_changeset.delete_option_ids {
select_ids.retain(|id| id != &delete_option_id);
}
new_cell_data = select_ids.join(SELECTION_IDS_SEPARATOR);
new_cell_data = select_ids.to_string();
tracing::trace!("Multi select cell data: {}", &new_cell_data);
}
}
@ -114,22 +117,86 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::type_options::selection_type_option::*;
use crate::services::field::FieldBuilder;
use crate::services::field::{MultiSelectTypeOptionBuilder, MultiSelectTypeOptionPB};
use flowy_grid_data_model::revision::FieldRevision;
#[test]
fn multi_select_test() {
let google_option = SelectOptionPB::new("Google");
let facebook_option = SelectOptionPB::new("Facebook");
let twitter_option = SelectOptionPB::new("Twitter");
fn multi_select_insert_multi_option_test() {
let google = SelectOptionPB::new("Google");
let facebook = SelectOptionPB::new("Facebook");
let multi_select = MultiSelectTypeOptionBuilder::default()
.add_option(google_option.clone())
.add_option(facebook_option.clone())
.add_option(twitter_option);
.add_option(google.clone())
.add_option(facebook.clone());
let field_rev = FieldBuilder::new(multi_select).name("Platform").build();
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google.id, facebook.id];
let data = SelectOptionCellChangeset::from_insert_options(option_ids.clone()).to_str();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
assert_eq!(&*select_option_ids, &option_ids);
}
#[test]
fn multi_select_unselect_multi_option_test() {
let google = SelectOptionPB::new("Google");
let facebook = SelectOptionPB::new("Facebook");
let multi_select = MultiSelectTypeOptionBuilder::default()
.add_option(google.clone())
.add_option(facebook.clone());
let field_rev = FieldBuilder::new(multi_select).name("Platform").build();
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google.id, facebook.id];
// insert
let data = SelectOptionCellChangeset::from_insert_options(option_ids.clone()).to_str();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
assert_eq!(&*select_option_ids, &option_ids);
// delete
let data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
assert!(select_option_ids.is_empty());
}
#[test]
fn multi_select_insert_single_option_test() {
let google = SelectOptionPB::new("Google");
let multi_select = MultiSelectTypeOptionBuilder::default().add_option(google.clone());
let field_rev = FieldBuilder::new(multi_select)
.name("Platform")
.visibility(true)
.build();
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let data = SelectOptionCellChangeset::from_insert_option_id(&google.id).to_str();
let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
assert_eq!(cell_option_ids, google.id);
}
#[test]
fn multi_select_insert_non_exist_option_test() {
let google = SelectOptionPB::new("Google");
let multi_select = MultiSelectTypeOptionBuilder::default();
let field_rev = FieldBuilder::new(multi_select)
.name("Platform")
.visibility(true)
.build();
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let data = SelectOptionCellChangeset::from_insert_option_id(&google.id).to_str();
let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
assert!(cell_option_ids.is_empty());
}
#[test]
fn multi_select_insert_invalid_option_id_test() {
let google = SelectOptionPB::new("Google");
let multi_select = MultiSelectTypeOptionBuilder::default().add_option(google);
let field_rev = FieldBuilder::new(multi_select)
.name("Platform")
@ -138,51 +205,23 @@ mod tests {
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR);
let data = SelectOptionCellChangeset::from_insert(&option_ids).to_str();
let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
assert_multi_select_options(
cell_data,
&type_option,
&field_rev,
vec![google_option.clone(), facebook_option],
);
// empty option id string
let data = SelectOptionCellChangeset::from_insert_option_id("").to_str();
let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
assert_eq!(cell_option_ids, "");
let data = SelectOptionCellChangeset::from_insert(&google_option.id).to_str();
let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
let data = SelectOptionCellChangeset::from_insert_option_id("123,456").to_str();
let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
assert_eq!(cell_option_ids, "");
}
// Invalid option id
let cell_data = type_option
.apply_changeset(SelectOptionCellChangeset::from_insert("").to_str().into(), None)
.unwrap();
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
#[test]
fn multi_select_invalid_changeset_data_test() {
let multi_select = MultiSelectTypeOptionBuilder::default();
let field_rev = FieldBuilder::new(multi_select).name("Platform").build();
let type_option = MultiSelectTypeOptionPB::from(&field_rev);
// Invalid option id
let cell_data = type_option
.apply_changeset(SelectOptionCellChangeset::from_insert("123,456").to_str().into(), None)
.unwrap();
assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]);
// Invalid changeset
// The type of the changeset should be SelectOptionCellChangeset
assert!(type_option.apply_changeset("123".to_owned().into(), None).is_err());
}
fn assert_multi_select_options(
cell_data: String,
type_option: &MultiSelectTypeOptionPB,
field_rev: &FieldRevision,
expected: Vec<SelectOptionPB>,
) {
let field_type: FieldType = field_rev.ty.into();
assert_eq!(
expected,
type_option
.decode_cell_data(cell_data.into(), &field_type, field_rev)
.unwrap()
.parser::<SelectOptionCellDataParser>()
.unwrap()
.select_options,
);
}
}

View File

@ -76,6 +76,8 @@ pub fn make_selected_select_options(
}
pub trait SelectOptionOperation: TypeOptionDataFormat + Send + Sync {
/// Insert the `SelectOptionPB` into corresponding type option.
/// Replace the old value if the option already exists in the option list.
fn insert_option(&mut self, new_option: SelectOptionPB) {
let options = self.mut_options();
if let Some(index) = options
@ -170,6 +172,11 @@ pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB {
}
}
/// List of select option ids
///
/// Calls [to_string] will return a string consists list of ids,
/// placing a commas separator between each
///
#[derive(Default)]
pub struct SelectOptionIds(Vec<String>);
@ -193,6 +200,10 @@ impl FromCellString for SelectOptionIds {
impl std::convert::From<String> for SelectOptionIds {
fn from(s: String) -> Self {
if s.is_empty() {
return Self(vec![]);
}
let ids = s
.split(SELECTION_IDS_SEPARATOR)
.map(|id| id.to_string())
@ -201,7 +212,16 @@ impl std::convert::From<String> for SelectOptionIds {
}
}
impl std::convert::From<Vec<String>> for SelectOptionIds {
fn from(ids: Vec<String>) -> Self {
let ids = ids.into_iter().filter(|id| !id.is_empty()).collect::<Vec<String>>();
Self(ids)
}
}
impl ToString for SelectOptionIds {
/// Returns a string that consists list of ids, placing a commas
/// separator between each
fn to_string(&self) -> String {
self.0.join(SELECTION_IDS_SEPARATOR)
}
@ -254,24 +274,24 @@ pub struct SelectOptionCellChangesetPayloadPB {
#[pb(index = 1)]
pub cell_identifier: GridCellIdPB,
#[pb(index = 2, one_of)]
pub insert_option_id: Option<String>,
#[pb(index = 2)]
pub insert_option_ids: Vec<String>,
#[pb(index = 3, one_of)]
pub delete_option_id: Option<String>,
#[pb(index = 3)]
pub delete_option_ids: Vec<String>,
}
pub struct SelectOptionCellChangesetParams {
pub cell_identifier: GridCellIdParams,
pub insert_option_id: Option<String>,
pub delete_option_id: Option<String>,
pub insert_option_ids: Vec<String>,
pub delete_option_ids: Vec<String>,
}
impl std::convert::From<SelectOptionCellChangesetParams> for CellChangesetPB {
fn from(params: SelectOptionCellChangesetParams) -> Self {
let changeset = SelectOptionCellChangeset {
insert_option_id: params.insert_option_id,
delete_option_id: params.delete_option_id,
insert_option_ids: params.insert_option_ids,
delete_option_ids: params.delete_option_ids,
};
let content = serde_json::to_string(&changeset).unwrap();
CellChangesetPB {
@ -288,36 +308,42 @@ impl TryInto<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPaylo
fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
let cell_identifier: GridCellIdParams = self.cell_identifier.try_into()?;
let insert_option_id = match self.insert_option_id {
None => None,
Some(insert_option_id) => Some(
NotEmptyStr::parse(insert_option_id)
.map_err(|_| ErrorCode::OptionIdIsEmpty)?
.0,
),
};
let insert_option_ids = self
.insert_option_ids
.into_iter()
.flat_map(|option_id| match NotEmptyStr::parse(option_id) {
Ok(option_id) => Some(option_id.0),
Err(_) => {
tracing::error!("The insert option id should not be empty");
None
}
})
.collect::<Vec<String>>();
let delete_option_id = match self.delete_option_id {
None => None,
Some(delete_option_id) => Some(
NotEmptyStr::parse(delete_option_id)
.map_err(|_| ErrorCode::OptionIdIsEmpty)?
.0,
),
};
let delete_option_ids = self
.delete_option_ids
.into_iter()
.flat_map(|option_id| match NotEmptyStr::parse(option_id) {
Ok(option_id) => Some(option_id.0),
Err(_) => {
tracing::error!("The deleted option id should not be empty");
None
}
})
.collect::<Vec<String>>();
Ok(SelectOptionCellChangesetParams {
cell_identifier,
insert_option_id,
delete_option_id,
insert_option_ids,
delete_option_ids,
})
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct SelectOptionCellChangeset {
pub insert_option_id: Option<String>,
pub delete_option_id: Option<String>,
pub insert_option_ids: Vec<String>,
pub delete_option_ids: Vec<String>,
}
impl FromCellChangeset for SelectOptionCellChangeset {
@ -330,17 +356,31 @@ impl FromCellChangeset for SelectOptionCellChangeset {
}
impl SelectOptionCellChangeset {
pub fn from_insert(option_id: &str) -> Self {
pub fn from_insert_option_id(option_id: &str) -> Self {
SelectOptionCellChangeset {
insert_option_id: Some(option_id.to_string()),
delete_option_id: None,
insert_option_ids: vec![option_id.to_string()],
delete_option_ids: vec![],
}
}
pub fn from_delete(option_id: &str) -> Self {
pub fn from_insert_options(option_ids: Vec<String>) -> Self {
SelectOptionCellChangeset {
insert_option_id: None,
delete_option_id: Some(option_id.to_string()),
insert_option_ids: option_ids,
delete_option_ids: vec![],
}
}
pub fn from_delete_option_id(option_id: &str) -> Self {
SelectOptionCellChangeset {
insert_option_ids: vec![],
delete_option_ids: vec![option_id.to_string()],
}
}
pub fn from_delete_options(option_ids: Vec<String>) -> Self {
SelectOptionCellChangeset {
insert_option_ids: vec![],
delete_option_ids: option_ids,
}
}
@ -369,21 +409,21 @@ pub struct SelectOptionChangesetPayloadPB {
#[pb(index = 1)]
pub cell_identifier: GridCellIdPB,
#[pb(index = 2, one_of)]
pub insert_option: Option<SelectOptionPB>,
#[pb(index = 2)]
pub insert_options: Vec<SelectOptionPB>,
#[pb(index = 3, one_of)]
pub update_option: Option<SelectOptionPB>,
#[pb(index = 3)]
pub update_options: Vec<SelectOptionPB>,
#[pb(index = 4, one_of)]
pub delete_option: Option<SelectOptionPB>,
#[pb(index = 4)]
pub delete_options: Vec<SelectOptionPB>,
}
pub struct SelectOptionChangeset {
pub cell_identifier: GridCellIdParams,
pub insert_option: Option<SelectOptionPB>,
pub update_option: Option<SelectOptionPB>,
pub delete_option: Option<SelectOptionPB>,
pub insert_options: Vec<SelectOptionPB>,
pub update_options: Vec<SelectOptionPB>,
pub delete_options: Vec<SelectOptionPB>,
}
impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayloadPB {
@ -393,9 +433,9 @@ impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayloadPB {
let cell_identifier = self.cell_identifier.try_into()?;
Ok(SelectOptionChangeset {
cell_identifier,
insert_option: self.insert_option,
update_option: self.update_option,
delete_option: self.delete_option,
insert_options: self.insert_options,
update_options: self.update_options,
delete_options: self.delete_options,
})
}
}

View File

@ -62,12 +62,24 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
changeset: CellDataChangeset<SelectOptionCellChangeset>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let select_option_changeset = changeset.try_into_inner()?;
let content_changeset = changeset.try_into_inner()?;
let new_cell_data: String;
if let Some(insert_option_id) = select_option_changeset.insert_option_id {
new_cell_data = insert_option_id;
} else {
let mut insert_option_ids = content_changeset
.insert_option_ids
.into_iter()
.filter(|insert_option_id| self.options.iter().any(|option| &option.id == insert_option_id))
.collect::<Vec<String>>();
// In single select, the insert_option_ids should only contain one select option id.
// Sometimes, the insert_option_ids may contain list of option ids. For example,
// copy/paste a ids string.
if insert_option_ids.is_empty() {
new_cell_data = "".to_string()
} else {
// Just take the first select option
let _ = insert_option_ids.drain(1..);
new_cell_data = insert_option_ids.pop().unwrap();
}
Ok(new_cell_data)
@ -98,71 +110,83 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::type_options::*;
use crate::services::field::FieldBuilder;
use flowy_grid_data_model::revision::FieldRevision;
#[test]
fn single_select_test() {
let google_option = SelectOptionPB::new("Google");
let facebook_option = SelectOptionPB::new("Facebook");
let twitter_option = SelectOptionPB::new("Twitter");
fn single_select_insert_multi_option_test() {
let google = SelectOptionPB::new("Google");
let facebook = SelectOptionPB::new("Facebook");
let single_select = SingleSelectTypeOptionBuilder::default()
.add_option(google_option.clone())
.add_option(facebook_option.clone())
.add_option(twitter_option);
.add_option(google.clone())
.add_option(facebook.clone());
let field_rev = FieldBuilder::new(single_select)
.name("Platform")
.visibility(true)
.build();
let field_rev = FieldBuilder::new(single_select).name("Platform").build();
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google.id.clone(), facebook.id];
let data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
assert_eq!(&*select_option_ids, &vec![google.id]);
}
#[test]
fn single_select_unselect_multi_option_test() {
let google = SelectOptionPB::new("Google");
let facebook = SelectOptionPB::new("Facebook");
let single_select = SingleSelectTypeOptionBuilder::default()
.add_option(google.clone())
.add_option(facebook.clone());
let field_rev = FieldBuilder::new(single_select).name("Platform").build();
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google.id.clone(), facebook.id];
// insert
let data = SelectOptionCellChangeset::from_insert_options(option_ids.clone()).to_str();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
assert_eq!(&*select_option_ids, &vec![google.id]);
// delete
let data = SelectOptionCellChangeset::from_delete_options(option_ids).to_str();
let select_option_ids: SelectOptionIds = type_option.apply_changeset(data.into(), None).unwrap().into();
assert!(select_option_ids.is_empty());
}
#[test]
fn single_select_insert_non_exist_option_test() {
let google = SelectOptionPB::new("Google");
let single_select = SingleSelectTypeOptionBuilder::default();
let field_rev = FieldBuilder::new(single_select).name("Platform").build();
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR);
let data = SelectOptionCellChangeset::from_insert(&option_ids).to_str();
let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option.clone()]);
let option_ids = vec![google.id];
let data = SelectOptionCellChangeset::from_insert_options(option_ids).to_str();
let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
let data = SelectOptionCellChangeset::from_insert(&google_option.id).to_str();
let cell_data = type_option.apply_changeset(data.into(), None).unwrap();
assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option]);
assert!(cell_option_ids.is_empty());
}
// Invalid option id
let cell_data = type_option
.apply_changeset(SelectOptionCellChangeset::from_insert("").to_str().into(), None)
.unwrap();
assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
#[test]
fn single_select_insert_invalid_option_id_test() {
let single_select = SingleSelectTypeOptionBuilder::default();
let field_rev = FieldBuilder::new(single_select).name("Platform").build();
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
// Invalid option id
let cell_data = type_option
.apply_changeset(SelectOptionCellChangeset::from_insert("123").to_str().into(), None)
.unwrap();
let data = SelectOptionCellChangeset::from_insert_option_id("").to_str();
let cell_option_ids = type_option.apply_changeset(data.into(), None).unwrap();
assert_eq!(cell_option_ids, "");
}
assert_single_select_options(cell_data, &type_option, &field_rev, vec![]);
#[test]
fn single_select_invalid_changeset_data_test() {
let single_select = SingleSelectTypeOptionBuilder::default();
let field_rev = FieldBuilder::new(single_select).name("Platform").build();
let type_option = SingleSelectTypeOptionPB::from(&field_rev);
// Invalid changeset
// The type of the changeset should be SelectOptionCellChangeset
assert!(type_option.apply_changeset("123".to_owned().into(), None).is_err());
}
fn assert_single_select_options(
cell_data: String,
type_option: &SingleSelectTypeOptionPB,
field_rev: &FieldRevision,
expected: Vec<SelectOptionPB>,
) {
let field_type: FieldType = field_rev.ty.into();
assert_eq!(
expected,
type_option
.decode_cell_data(cell_data.into(), &field_type, field_rev)
.unwrap()
.parser::<SelectOptionCellDataParser>()
.unwrap()
.select_options,
);
}
}

View File

@ -62,7 +62,7 @@ impl GroupController for MultiSelectGroupController {
match self.group_ctx.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, group)) => {
let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
let cell_rev = insert_select_option_cell(vec![group.id.clone()], field_rev);
row_rev.cells.insert(field_rev.id.clone(), cell_rev);
}
}

View File

@ -63,7 +63,7 @@ impl GroupController for SingleSelectGroupController {
match group {
None => {}
Some(group) => {
let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
let cell_rev = insert_select_option_cell(vec![group.id.clone()], field_rev);
row_rev.cells.insert(field_rev.id.clone(), cell_rev);
}
}

View File

@ -134,11 +134,11 @@ pub fn make_inserted_cell_rev(group_id: &str, field_rev: &FieldRevision) -> Opti
let field_type: FieldType = field_rev.ty.into();
match field_type {
FieldType::SingleSelect => {
let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
let cell_rev = insert_select_option_cell(vec![group_id.to_owned()], field_rev);
Some(cell_rev)
}
FieldType::MultiSelect => {
let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
let cell_rev = insert_select_option_cell(vec![group_id.to_owned()], field_rev);
Some(cell_rev)
}
FieldType::Checkbox => {

View File

@ -92,13 +92,13 @@ impl<'a> RowRevisionBuilder<'a> {
}
}
pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) {
pub fn insert_select_option_cell(&mut self, field_id: &str, option_ids: Vec<String>) {
match self.field_rev_map.get(&field_id.to_owned()) {
None => tracing::warn!("Can't find the select option field with id: {}", field_id),
Some(field_rev) => {
self.payload
.cell_by_field_id
.insert(field_id.to_owned(), insert_select_option_cell(data, field_rev));
.insert(field_id.to_owned(), insert_select_option_cell(option_ids, field_rev));
}
}
}

View File

@ -60,7 +60,7 @@ pub fn make_default_board() -> BuildGridContext {
for i in 0..3 {
let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
row_builder.insert_select_option_cell(&single_select_field_id, to_do_option.id.clone());
row_builder.insert_select_option_cell(&single_select_field_id, vec![to_do_option.id.clone()]);
let data = format!("Card {}", i + 1);
row_builder.insert_text_cell(&text_field_id, data);
let row = row_builder.build();
@ -116,23 +116,23 @@ pub fn make_default_board_2() -> BuildGridContext {
for i in 0..3 {
let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
row_builder.insert_select_option_cell(&single_select_field_id, to_do_option.id.clone());
row_builder.insert_select_option_cell(&single_select_field_id, vec![to_do_option.id.clone()]);
match i {
0 => {
row_builder.insert_text_cell(&text_field_id, "Update AppFlowy Website".to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![work_option.id.clone()]);
}
1 => {
row_builder.insert_text_cell(&text_field_id, "Learn French".to_string());
let mut options = SelectOptionIds::new();
options.push(fun_option.id.clone());
options.push(travel_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![options.to_string()]);
}
2 => {
row_builder.insert_text_cell(&text_field_id, "Exercise 4x/week".to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![fun_option.id.clone()]);
}
_ => {}
}
@ -142,15 +142,15 @@ pub fn make_default_board_2() -> BuildGridContext {
for i in 0..3 {
let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
row_builder.insert_select_option_cell(&single_select_field_id, doing_option.id.clone());
row_builder.insert_select_option_cell(&single_select_field_id, vec![doing_option.id.clone()]);
match i {
0 => {
row_builder.insert_text_cell(&text_field_id, "Learn how to swim".to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, fun_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![fun_option.id.clone()]);
}
1 => {
row_builder.insert_text_cell(&text_field_id, "Meditate 10 mins each day".to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, health_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![health_option.id.clone()]);
}
2 => {
@ -158,7 +158,7 @@ pub fn make_default_board_2() -> BuildGridContext {
let mut options = SelectOptionIds::new();
options.push(fun_option.id.clone());
options.push(work_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![options.to_string()]);
}
_ => {}
}
@ -168,18 +168,18 @@ pub fn make_default_board_2() -> BuildGridContext {
for i in 0..2 {
let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs());
row_builder.insert_select_option_cell(&single_select_field_id, done_option.id.clone());
row_builder.insert_select_option_cell(&single_select_field_id, vec![done_option.id.clone()]);
match i {
0 => {
row_builder.insert_text_cell(&text_field_id, "Publish an article".to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, work_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![work_option.id.clone()]);
}
1 => {
row_builder.insert_text_cell(&text_field_id, "Visit Chicago".to_string());
let mut options = SelectOptionIds::new();
options.push(travel_option.id.clone());
options.push(fun_option.id.clone());
row_builder.insert_select_option_cell(&multi_select_field_id, options.to_string());
row_builder.insert_select_option_cell(&multi_select_field_id, vec![options.to_string()]);
}
_ => {}

View File

@ -2,7 +2,7 @@ use flowy_grid::entities::FieldType;
use std::sync::Arc;
use flowy_grid::services::field::{
DateCellChangesetPB, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB, SELECTION_IDS_SEPARATOR,
DateCellChangesetPB, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB,
};
use flowy_grid::services::row::RowRevisionBuilder;
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
@ -70,7 +70,7 @@ impl<'a> GridRowTestBuilder<'a> {
let type_option = SingleSelectTypeOptionPB::from(&single_select_field);
let option = f(type_option.options);
self.inner_builder
.insert_select_option_cell(&single_select_field.id, option.id);
.insert_select_option_cell(&single_select_field.id, vec![option.id]);
single_select_field.id.clone()
}
@ -82,11 +82,7 @@ impl<'a> GridRowTestBuilder<'a> {
let multi_select_field = self.field_rev_with_type(&FieldType::MultiSelect);
let type_option = MultiSelectTypeOptionPB::from(&multi_select_field);
let options = f(type_option.options);
let ops_ids = options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>()
.join(SELECTION_IDS_SEPARATOR);
let ops_ids = options.iter().map(|option| option.id.clone()).collect::<Vec<_>>();
self.inner_builder
.insert_select_option_cell(&multi_select_field.id, ops_ids);

View File

@ -25,11 +25,11 @@ async fn grid_cell_update() {
FieldType::DateTime => make_date_cell_string("123"),
FieldType::SingleSelect => {
let type_option = SingleSelectTypeOptionPB::from(field_rev);
SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str()
SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id).to_str()
}
FieldType::MultiSelect => {
let type_option = MultiSelectTypeOptionPB::from(field_rev);
SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str()
SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id).to_str()
}
FieldType::Checkbox => "1".to_string(),
FieldType::URL => "1".to_string(),

View File

@ -136,16 +136,24 @@ impl GridGroupTest {
let cell_rev = if to_group.is_default {
match field_type {
FieldType::SingleSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev),
FieldType::MultiSelect => delete_select_option_cell(to_group.group_id.clone(), &field_rev),
FieldType::SingleSelect => {
delete_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
}
FieldType::MultiSelect => {
delete_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
}
_ => {
panic!("Unsupported group field type");
}
}
} else {
match field_type {
FieldType::SingleSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev),
FieldType::MultiSelect => insert_select_option_cell(to_group.group_id.clone(), &field_rev),
FieldType::SingleSelect => {
insert_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
}
FieldType::MultiSelect => {
insert_select_option_cell(vec![to_group.group_id.clone()], &field_rev)
}
_ => {
panic!("Unsupported group field type");
}