refactor: group controller traits (#4880)

* chore: remove async trait

* chore: require From<AnyMap> for trait TypeOption

* refactor: simplify group controller by removing GroupController trait

* chore: rename GroupControllerOperation trait to GroupController

* chore: add some docs

* chore: remove plural struct and use Vec directly

* chore: unnecessary mut

* chore: use same name to indicate passthrough

* chore: remove unused trait

* chore: rename group context to group controller context

* chore: remove group name

* chore: move type option interceptor to GroupCustomize and split off delegates for group context and group controller

* chore: adapt tests to changes

* chore: adapt flutter frontend to changes

* chore: code cleanup

* chore: fix clippy and adapt tauri frontend to changes

* chore: group controller code clean up

* chore: no need to pass cell data when moving row

* chore: rename some functions and variables

* chore: remove content filter
This commit is contained in:
Richard Shiue 2024-03-16 17:18:40 +08:00 committed by GitHub
parent 0f006fa60b
commit b557f89829
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 490 additions and 962 deletions

View File

@ -199,7 +199,7 @@ class MobileHiddenGroup extends StatelessWidget {
children: [
Expanded(
child: Text(
group.groupName,
context.read<BoardBloc>().generateGroupNameFromGroup(group),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,

View File

@ -11,9 +11,11 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../../application/database_controller.dart';
@ -397,7 +399,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
boardController.getGroupController(group.groupId);
if (columnController != null) {
// remove the group or update its name
columnController.updateGroupName(group.groupName);
columnController.updateGroupName(generateGroupNameFromGroup(group));
if (!group.isVisible) {
boardController.removeGroup(group.groupId);
}
@ -491,7 +493,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
AppFlowyGroupData _initializeGroupData(GroupPB group) {
return AppFlowyGroupData(
id: group.groupId,
name: group.groupName,
name: generateGroupNameFromGroup(group),
items: _buildGroupItems(group),
customData: GroupData(
group: group,
@ -499,6 +501,72 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
),
);
}
String generateGroupNameFromGroup(GroupPB group) {
final field = fieldController.getField(group.fieldId);
if (field == null) {
return "";
}
// if the group is the default group, then
if (group.isDefault) {
return "No ${field.name}";
}
switch (field.fieldType) {
case FieldType.SingleSelect:
final options =
SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
.options;
final option =
options.firstWhereOrNull((option) => option.id == group.groupId);
return option == null ? "" : option.name;
case FieldType.MultiSelect:
final options =
MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
.options;
final option =
options.firstWhereOrNull((option) => option.id == group.groupId);
return option == null ? "" : option.name;
case FieldType.Checkbox:
return group.groupId;
case FieldType.URL:
return group.groupId;
case FieldType.DateTime:
// Assume DateCondition::Relative as there isn't an option for this
// right now.
final dateFormat = DateFormat("y/MM/dd");
try {
final targetDateTime = dateFormat.parseLoose(group.groupId);
final targetDateTimeDay = DateTime(
targetDateTime.year,
targetDateTime.month,
targetDateTime.day,
);
final now = DateTime.now();
final nowDay = DateTime(
now.year,
now.month,
now.day,
);
final diff = targetDateTimeDay.difference(nowDay).inDays;
return switch (diff) {
0 => "Today",
-1 => "Yesterday",
1 => "Tomorrow",
-7 => "Last 7 days",
2 => "Next 7 days",
-30 => "Last 30 days",
8 => "Next 30 days",
_ => DateFormat("MMM y").format(targetDateTimeDay)
};
} on FormatException {
return "";
}
default:
return "";
}
}
}
@freezed

View File

@ -31,23 +31,11 @@ class GroupController {
final GroupControllerDelegate delegate;
final void Function(GroupPB group) onGroupChanged;
RowMetaPB? rowAtIndex(int index) {
if (index < group.rows.length) {
return group.rows[index];
} else {
return null;
}
}
RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index);
RowMetaPB? firstRow() {
if (group.rows.isEmpty) return null;
return group.rows.first;
}
RowMetaPB? firstRow() => group.rows.firstOrNull;
RowMetaPB? lastRow() {
if (group.rows.isEmpty) return null;
return group.rows.last;
}
RowMetaPB? lastRow() => group.rows.lastOrNull;
void startListening() {
_listener.start(

View File

@ -269,7 +269,7 @@ class HiddenGroupButtonContent extends StatelessWidget {
),
const HSpace(4),
FlowyText.medium(
group.groupName,
bloc.generateGroupNameFromGroup(group),
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
@ -369,7 +369,7 @@ class HiddenGroupPopupItemList extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
group.groupName,
context.read<BoardBloc>().generateGroupNameFromGroup(group),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,

View File

@ -49,12 +49,6 @@ void main() {
boardBloc.groupControllers.values.length == 1,
"Expected 1, but receive ${boardBloc.groupControllers.values.length}",
);
final expectedGroupName = "No ${multiSelectField.name}";
assert(
boardBloc.groupControllers.values.first.group.groupName ==
expectedGroupName,
"Expected $expectedGroupName, but receive ${boardBloc.groupControllers.values.first.group.groupName}",
);
});
test('group by multi select with no options test', () async {
@ -105,11 +99,5 @@ void main() {
boardBloc.groupControllers.values.length == 3,
"Expected 3, but receive ${boardBloc.groupControllers.values.length}",
);
final groups =
boardBloc.groupControllers.values.map((e) => e.group).toList();
assert(groups[0].groupName == "No ${multiSelectField.name}");
assert(groups[1].groupName == "B");
assert(groups[2].groupName == "A");
});
}

View File

@ -8,7 +8,6 @@ export interface GroupSetting {
export interface Group {
id: string;
name: string;
isDefault: boolean;
isVisible: boolean;
fieldId: string;
@ -18,7 +17,6 @@ export interface Group {
export function pbToGroup(pb: GroupPB): Group {
return {
id: pb.group_id,
name: pb.group_name,
isDefault: pb.is_default,
isVisible: pb.is_visible,
fieldId: pb.field_id,

View File

@ -89,7 +89,6 @@ async fn rename_group_event_test() {
.create_board(&current_workspace.id, "my board view".to_owned(), vec![])
.await;
// Empty to group id
let groups = test.get_groups(&board_view.id).await;
let error = test
.update_group(
@ -101,9 +100,6 @@ async fn rename_group_event_test() {
)
.await;
assert!(error.is_none());
let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups[1].group_name, "new name".to_owned());
}
#[tokio::test]
@ -144,9 +140,6 @@ async fn update_group_name_test() {
let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups.len(), 4);
assert_eq!(groups[1].group_name, "To Do");
assert_eq!(groups[2].group_name, "Doing");
assert_eq!(groups[3].group_name, "Done");
test
.update_group(
@ -160,8 +153,6 @@ async fn update_group_name_test() {
let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups.len(), 4);
assert_eq!(groups[1].group_name, "To Do?");
assert_eq!(groups[2].group_name, "Doing");
}
#[tokio::test]
@ -174,14 +165,9 @@ async fn delete_group_test() {
let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups.len(), 4);
assert_eq!(groups[1].group_name, "To Do");
assert_eq!(groups[2].group_name, "Doing");
assert_eq!(groups[3].group_name, "Done");
test.delete_group(&board_view.id, &groups[1].group_id).await;
let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups.len(), 3);
assert_eq!(groups[1].group_name, "Doing");
assert_eq!(groups[2].group_name, "Done");
}

View File

@ -76,9 +76,6 @@ pub struct GroupPB {
#[pb(index = 2)]
pub group_id: String,
#[pb(index = 3)]
pub group_name: String,
#[pb(index = 4)]
pub rows: Vec<RowMetaPB>,
@ -94,7 +91,6 @@ impl std::convert::From<GroupData> for GroupPB {
Self {
field_id: group_data.field_id,
group_id: group_data.id,
group_name: group_data.name,
rows: group_data.rows.into_iter().map(RowMetaPB::from).collect(),
is_default: group_data.is_default,
is_visible: group_data.is_visible,

View File

@ -11,16 +11,13 @@ pub struct GroupRowsNotificationPB {
#[pb(index = 1)]
pub group_id: String,
#[pb(index = 2, one_of)]
pub group_name: Option<String>,
#[pb(index = 3)]
#[pb(index = 2)]
pub inserted_rows: Vec<InsertedRowPB>,
#[pb(index = 4)]
#[pb(index = 3)]
pub deleted_rows: Vec<String>,
#[pb(index = 5)]
#[pb(index = 4)]
pub updated_rows: Vec<RowMetaPB>,
}
@ -43,10 +40,7 @@ impl std::fmt::Display for GroupRowsNotificationPB {
impl GroupRowsNotificationPB {
pub fn is_empty(&self) -> bool {
self.group_name.is_none()
&& self.inserted_rows.is_empty()
&& self.deleted_rows.is_empty()
&& self.updated_rows.is_empty()
self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty()
}
pub fn new(group_id: String) -> Self {
@ -56,14 +50,6 @@ impl GroupRowsNotificationPB {
}
}
pub fn name(group_id: String, name: &str) -> Self {
Self {
group_id,
group_name: Some(name.to_owned()),
..Default::default()
}
}
pub fn insert(group_id: String, inserted_rows: Vec<InsertedRowPB>) -> Self {
Self {
group_id,

View File

@ -668,7 +668,7 @@ pub(crate) async fn update_group_handler(
let (tx, rx) = oneshot::channel();
af_spawn(async move {
let result = database_editor
.update_group(&view_id, vec![group_changeset].into())
.update_group(&view_id, vec![group_changeset])
.await;
let _ = tx.send(result);
});

View File

@ -35,7 +35,7 @@ use crate::services::field_settings::{
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
};
use crate::services::filter::{Filter, FilterChangeset};
use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset};
use crate::services::group::{default_group_setting, GroupChangeset, GroupSetting, RowChangeset};
use crate::services::share::csv::{CSVExport, CSVFormat};
use crate::services::sort::Sort;
use crate::utils::cache::AnyTypeCache;
@ -210,7 +210,11 @@ impl DatabaseEditor {
Ok(self.database.lock().delete_view(view_id))
}
pub async fn update_group(&self, view_id: &str, changesets: GroupChangesets) -> FlowyResult<()> {
pub async fn update_group(
&self,
view_id: &str,
changesets: Vec<GroupChangeset>,
) -> FlowyResult<()> {
let view_editor = self.database_views.get_view_editor(view_id).await?;
view_editor.v_update_group(changesets).await?;
Ok(())

View File

@ -37,7 +37,7 @@ use crate::services::database_view::{
};
use crate::services::field_settings::FieldSettings;
use crate::services::filter::{Filter, FilterChangeset, FilterController};
use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowContext, RowChangeset};
use crate::services::group::{GroupChangeset, GroupController, MoveGroupRowContext, RowChangeset};
use crate::services::setting::CalendarLayoutSetting;
use crate::services::sort::{Sort, SortChangeset, SortController};
@ -139,12 +139,13 @@ impl DatabaseViewEditor {
// fill in cells according to group_id if supplied
if let Some(group_id) = params.group_id {
let _ = self
.mut_group_controller(|group_controller, field| {
group_controller.will_create_row(&mut cells, &field, &group_id);
Ok(())
})
.await;
if let Some(controller) = self.group_controller.read().await.as_ref() {
let field = self
.delegate
.get_field(controller.field_id())
.ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?;
controller.will_create_row(&mut cells, &field, &group_id);
}
}
// fill in cells according to active filters
@ -460,13 +461,12 @@ impl DatabaseViewEditor {
Ok(changes)
}
pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> {
pub async fn v_update_group(&self, changeset: Vec<GroupChangeset>) -> FlowyResult<()> {
let mut type_option_data = TypeOptionData::new();
let (old_field, updated_groups) = if let Some(controller) =
self.group_controller.write().await.as_mut()
{
let (old_field, updated_groups) =
if let Some(controller) = self.group_controller.write().await.as_mut() {
let old_field = self.delegate.get_field(controller.field_id());
let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?;
let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset)?;
type_option_data.extend(new_type_option);
(old_field, updated_groups)
@ -800,13 +800,6 @@ impl DatabaseViewEditor {
.did_update_field_type_option(&field)
.await;
self
.mut_group_controller(|group_controller, _| {
group_controller.did_update_field_type_option(&field);
Ok(())
})
.await;
if old_field.field_type != field.field_type {
let changeset = FilterChangeset::DeleteAllWithFieldId {
field_id: field.id.clone(),

View File

@ -1,8 +1,7 @@
use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::Field;
use collab_database::rows::{Cell, RowId};
use collab_database::rows::RowId;
use flowy_error::FlowyResult;
use lib_infra::future::{to_fut, Fut};
@ -11,8 +10,8 @@ use crate::entities::FieldType;
use crate::services::database_view::DatabaseViewOperation;
use crate::services::field::RowSingleCellData;
use crate::services::group::{
find_new_grouping_field, make_group_controller, GroupController, GroupSetting,
GroupSettingReader, GroupSettingWriter, GroupTypeOptionCellOperation,
find_suitable_grouping_field, make_group_controller, GroupContextDelegate, GroupController,
GroupControllerDelegate, GroupSetting,
};
pub async fn new_group_controller_with_field(
@ -20,19 +19,9 @@ pub async fn new_group_controller_with_field(
delegate: Arc<dyn DatabaseViewOperation>,
grouping_field: Field,
) -> FlowyResult<Box<dyn GroupController>> {
let setting_reader = GroupSettingReaderImpl(delegate.clone());
let configuration_delegate = GroupControllerDelegateImpl(delegate.clone());
let rows = delegate.get_rows(&view_id).await;
let setting_writer = GroupSettingWriterImpl(delegate.clone());
let type_option_writer = GroupTypeOptionCellWriterImpl(delegate.clone());
make_group_controller(
view_id,
grouping_field,
rows,
setting_reader,
setting_writer,
type_option_writer,
)
.await
make_group_controller(view_id, grouping_field, rows, configuration_delegate).await
}
pub async fn new_group_controller(
@ -40,10 +29,10 @@ pub async fn new_group_controller(
delegate: Arc<dyn DatabaseViewOperation>,
) -> FlowyResult<Option<Box<dyn GroupController>>> {
let fields = delegate.get_fields(&view_id, None).await;
let setting_reader = GroupSettingReaderImpl(delegate.clone());
let controller_delegate = GroupControllerDelegateImpl(delegate.clone());
// Read the grouping field or find a new grouping field
let mut grouping_field = setting_reader
let mut grouping_field = controller_delegate
.get_group_setting(&view_id)
.await
.and_then(|setting| {
@ -56,32 +45,22 @@ pub async fn new_group_controller(
let layout = delegate.get_layout_for_view(&view_id);
// If the view is a board and the grouping field is empty, we need to find a new grouping field
if layout.is_board() && grouping_field.is_none() {
grouping_field = find_new_grouping_field(&fields, &layout);
grouping_field = find_suitable_grouping_field(&fields);
}
if let Some(grouping_field) = grouping_field {
let rows = delegate.get_rows(&view_id).await;
let setting_writer = GroupSettingWriterImpl(delegate.clone());
let type_option_writer = GroupTypeOptionCellWriterImpl(delegate.clone());
Ok(Some(
make_group_controller(
view_id,
grouping_field,
rows,
setting_reader,
setting_writer,
type_option_writer,
)
.await?,
make_group_controller(view_id, grouping_field, rows, controller_delegate).await?,
))
} else {
Ok(None)
}
}
pub(crate) struct GroupSettingReaderImpl(pub Arc<dyn DatabaseViewOperation>);
pub(crate) struct GroupControllerDelegateImpl(pub Arc<dyn DatabaseViewOperation>);
impl GroupSettingReader for GroupSettingReaderImpl {
impl GroupContextDelegate for GroupControllerDelegateImpl {
fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>> {
let mut settings = self.0.get_group_setting(view_id);
to_fut(async move {
@ -99,6 +78,17 @@ impl GroupSettingReader for GroupSettingReaderImpl {
let delegate = self.0.clone();
to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await })
}
fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>> {
self.0.insert_group_setting(view_id, group_setting);
to_fut(async move { Ok(()) })
}
}
impl GroupControllerDelegate for GroupControllerDelegateImpl {
fn get_field(&self, field_id: &str) -> Option<Field> {
self.0.get_field(field_id)
}
}
pub(crate) async fn get_cell_for_row(
@ -153,31 +143,3 @@ pub(crate) async fn get_cells_for_field(
vec![]
}
struct GroupSettingWriterImpl(Arc<dyn DatabaseViewOperation>);
impl GroupSettingWriter for GroupSettingWriterImpl {
fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>> {
self.0.insert_group_setting(view_id, group_setting);
to_fut(async move { Ok(()) })
}
}
struct GroupTypeOptionCellWriterImpl(Arc<dyn DatabaseViewOperation>);
#[async_trait]
impl GroupTypeOptionCellOperation for GroupTypeOptionCellWriterImpl {
async fn get_cell(&self, _row_id: &RowId, _field_id: &str) -> FlowyResult<Option<Cell>> {
todo!()
}
#[tracing::instrument(level = "trace", skip_all, err)]
async fn update_cell(
&self,
_view_id: &str,
_row_id: &RowId,
_field_id: &str,
_cell: Cell,
) -> FlowyResult<()> {
todo!()
}
}

View File

@ -2,17 +2,14 @@ use std::collections::HashMap;
use std::sync::Arc;
use collab_database::database::MutexDatabase;
use collab_database::rows::{RowDetail, RowId};
use nanoid::nanoid;
use tokio::sync::{broadcast, RwLock};
use flowy_error::{FlowyError, FlowyResult};
use lib_infra::future::Fut;
use crate::services::cell::CellCache;
use crate::services::database::DatabaseRowEvent;
use crate::services::database_view::{DatabaseViewEditor, DatabaseViewOperation};
use crate::services::group::RowChangeset;
pub type RowEventSender = broadcast::Sender<DatabaseRowEvent>;
pub type RowEventReceiver = broadcast::Receiver<DatabaseRowEvent>;
@ -59,30 +56,6 @@ impl DatabaseViews {
.collect()
}
/// It may generate a RowChangeset when the Row was moved from one group to another.
/// The return value, [RowChangeset], contains the changes made by the groups.
///
pub async fn move_group_row(
&self,
view_id: &str,
row_detail: Arc<RowDetail>,
to_group_id: String,
to_row_id: Option<RowId>,
recv_row_changeset: impl FnOnce(RowChangeset) -> Fut<()>,
) -> FlowyResult<()> {
let view_editor = self.get_view_editor(view_id).await?;
let mut row_changeset = RowChangeset::new(row_detail.row.id.clone());
view_editor
.v_move_group_row(&row_detail, &mut row_changeset, &to_group_id, to_row_id)
.await;
if !row_changeset.is_empty() {
recv_row_changeset(row_changeset).await;
}
Ok(())
}
pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<DatabaseViewEditor>> {
debug_assert!(!view_id.is_empty());
if let Some(editor) = self.editor_by_view_id.read().await.get(view_id) {

View File

@ -22,7 +22,7 @@ use crate::services::field::{
use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter};
use crate::services::sort::SortCondition;
pub trait TypeOption {
pub trait TypeOption: From<TypeOptionData> + Into<TypeOptionData> {
/// `CellData` represents the decoded model for the current type option. Each of them must
/// implement the From<&Cell> trait. If the `Cell` cannot be decoded into this type, the default
/// value will be returned.

View File

@ -138,7 +138,7 @@ where
cell: &Cell,
decoded_field_type: &FieldType,
field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
) -> 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();
@ -168,12 +168,7 @@ where
Ok(cell_data)
}
fn set_decoded_cell_data(
&self,
cell: &Cell,
cell_data: <Self as TypeOption>::CellData,
field: &Field,
) {
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);
@ -196,16 +191,6 @@ impl<T> std::ops::Deref for TypeOptionCellDataHandlerImpl<T> {
}
}
impl<T> TypeOption for TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption + Send + Sync,
{
type CellData = T::CellData;
type CellChangeset = T::CellChangeset;
type CellProtobufType = T::CellProtobufType;
type CellFilter = T::CellFilter;
}
impl<T> TypeOptionCellDataHandler for TypeOptionCellDataHandlerImpl<T>
where
T: TypeOption
@ -227,7 +212,7 @@ where
) -> FlowyResult<CellProtobufBlob> {
let cell_data = self
.get_cell_data(cell, decoded_field_type, field_rev)?
.unbox_or_default::<<Self as TypeOption>::CellData>();
.unbox_or_default::<T::CellData>();
CellProtobufBlob::from(self.protobuf_encode(cell_data))
}
@ -238,7 +223,7 @@ where
old_cell: Option<Cell>,
field: &Field,
) -> FlowyResult<Cell> {
let changeset = cell_changeset.unbox_or_error::<<Self as TypeOption>::CellChangeset>()?;
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)
@ -306,7 +291,7 @@ where
fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool {
let perform_filter = || {
let field_type = FieldType::from(field.field_type);
let cell_filter = filter.downcast_ref::<<Self as TypeOption>::CellFilter>()?;
let cell_filter = filter.downcast_ref::<T::CellFilter>()?;
let cell_data = self.get_decoded_cell_data(cell, &field_type, field).ok()?;
Some(self.apply_filter(cell_filter, &cell_data))
};

View File

@ -1,16 +1,15 @@
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, Row, RowDetail, RowId};
use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId};
use flowy_error::FlowyResult;
use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB};
use crate::services::field::TypeOption;
use crate::services::group::{GroupChangesets, GroupData, MoveGroupRowContext};
use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext};
/// Using polymorphism to provides the customs action for different group controller.
///
/// For example, the `CheckboxGroupController` implements this trait to provide custom behavior.
/// [GroupCustomize] is implemented by parameterized `BaseGroupController`s to provide different
/// behaviors. This allows the BaseGroupController to call these actions indescriminantly using
/// polymorphism.
///
pub trait GroupCustomize: Send + Sync {
type GroupTypeOption: TypeOption;
@ -57,11 +56,7 @@ pub trait GroupCustomize: Send + Sync {
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>);
/// Move row from one group to another
fn move_row(
&mut self,
cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB>;
fn move_row(&mut self, context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB>;
/// Returns None if there is no need to delete the group when corresponding row get removed
fn delete_group_when_move_row(
@ -72,19 +67,35 @@ pub trait GroupCustomize: Send + Sync {
None
}
fn generate_new_group(
fn create_group(
&mut self,
_name: String,
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
Ok((None, None))
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>>;
fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>>;
fn update_type_option_when_update_group(
&mut self,
_changeset: &GroupChangeset,
_type_option: &mut Self::GroupTypeOption,
) {
}
/// Defines the shared actions any group controller can perform.
#[async_trait]
pub trait GroupControllerOperation: Send + Sync {
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str);
}
/// The `GroupController` trait defines the behavior of the group controller when performing any
/// group-related tasks, such as managing rows within a group, transferring rows between groups,
/// manipulating groups themselves, and even pre-filling a row's cells before it is created.
///
/// Depending on the type of the field that is being grouped, a parameterized `BaseGroupController`
/// or a `DefaultGroupController` may be the actual object that provides the functionality of
/// this trait. For example, a `Single-Select` group controller will be a `BaseGroupController`,
/// while a `URL` group controller will be a `DefaultGroupController`.
///
pub trait GroupController: Send + Sync {
/// Returns the id of field that is being used to group the rows
fn field_id(&self) -> &str;
@ -175,10 +186,13 @@ pub trait GroupControllerOperation: Send + Sync {
/// in the field type option data.
///
/// * `changesets`: list of changesets to be made to one or more groups
async fn apply_group_changeset(
fn apply_group_changeset(
&mut self,
changesets: &GroupChangesets,
changesets: &[GroupChangeset],
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)>;
/// Called before the row was created.
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str);
}
#[derive(Debug)]

View File

@ -1,11 +1,8 @@
use std::collections::HashMap;
use std::fmt::Formatter;
use std::marker::PhantomData;
use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::Field;
use collab_database::rows::{Cell, RowId};
use indexmap::IndexMap;
use serde::de::DeserializeOwned;
use serde::Serialize;
@ -21,28 +18,15 @@ use crate::services::group::{
default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting,
};
pub trait GroupSettingReader: Send + Sync + 'static {
pub trait GroupContextDelegate: Send + Sync + 'static {
fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>>;
fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>>;
}
pub trait GroupSettingWriter: Send + Sync + 'static {
fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>>;
fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>>;
}
#[async_trait]
pub trait GroupTypeOptionCellOperation: Send + Sync + 'static {
async fn get_cell(&self, row_id: &RowId, field_id: &str) -> FlowyResult<Option<Cell>>;
async fn update_cell(
&self,
view_id: &str,
row_id: &RowId,
field_id: &str,
cell: Cell,
) -> FlowyResult<()>;
}
impl<T> std::fmt::Display for GroupContext<T> {
impl<T> std::fmt::Display for GroupControllerContext<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.group_by_id.iter().for_each(|(_, group)| {
let _ = f.write_fmt(format_args!(
@ -56,12 +40,12 @@ impl<T> std::fmt::Display for GroupContext<T> {
}
}
/// A [GroupContext] represents as the groups memory cache
/// Each [GenericGroupController] has its own [GroupContext], the `context` has its own configuration
/// A [GroupControllerContext] represents as the groups memory cache
/// Each [GenericGroupController] has its own [GroupControllerContext], the `context` has its own configuration
/// that is restored from the disk.
///
/// The `context` contains a list of [GroupData]s and the grouping [Field]
pub struct GroupContext<C> {
pub struct GroupControllerContext<C> {
pub view_id: String,
/// The group configuration restored from the disk.
///
@ -70,24 +54,18 @@ pub struct GroupContext<C> {
configuration_phantom: PhantomData<C>,
/// The grouping field
field: Field,
/// The grouping field id
field_id: String,
/// Cache all the groups. Cache the group by its id.
/// We use the id of the [Field] as the [No Status] group id.
group_by_id: IndexMap<String, GroupData>,
/// A reader that implement the [GroupSettingReader] trait
///
reader: Arc<dyn GroupSettingReader>,
/// A writer that implement the [GroupSettingWriter] trait is used to save the
/// configuration to disk
///
writer: Arc<dyn GroupSettingWriter>,
/// delegate that reads and writes data to and from disk
delegate: Arc<dyn GroupContextDelegate>,
}
impl<C> GroupContext<C>
impl<C> GroupControllerContext<C>
where
C: Serialize + DeserializeOwned,
{
@ -95,14 +73,13 @@ where
pub async fn new(
view_id: String,
field: Field,
reader: Arc<dyn GroupSettingReader>,
writer: Arc<dyn GroupSettingWriter>,
delegate: Arc<dyn GroupContextDelegate>,
) -> FlowyResult<Self> {
event!(tracing::Level::TRACE, "GroupContext::new");
let setting = match reader.get_group_setting(&view_id).await {
event!(tracing::Level::TRACE, "GroupControllerContext::new");
let setting = match delegate.get_group_setting(&view_id).await {
None => {
let default_configuration = default_group_setting(&field);
writer
delegate
.save_configuration(&view_id, default_configuration.clone())
.await?;
Arc::new(default_configuration)
@ -112,10 +89,9 @@ where
Ok(Self {
view_id,
field,
field_id: field.id,
group_by_id: IndexMap::new(),
reader,
writer,
delegate,
setting,
configuration_phantom: PhantomData,
})
@ -126,11 +102,11 @@ where
/// We take the `id` of the `field` as the no status group id
#[allow(dead_code)]
pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> {
self.group_by_id.get(&self.field.id)
self.group_by_id.get(&self.field_id)
}
pub(crate) fn get_mut_no_status_group(&mut self) -> Option<&mut GroupData> {
self.group_by_id.get_mut(&self.field.id)
self.group_by_id.get_mut(&self.field_id)
}
pub(crate) fn groups(&self) -> Vec<&GroupData> {
@ -155,7 +131,7 @@ where
/// Iterate mut the groups without `No status` group
pub(crate) fn iter_mut_status_groups(&mut self, mut each: impl FnMut(&mut GroupData)) {
self.group_by_id.iter_mut().for_each(|(_, group)| {
if group.id != self.field.id {
if group.id != self.field_id {
each(group);
}
});
@ -168,13 +144,7 @@ where
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub(crate) fn add_new_group(&mut self, group: Group) -> FlowyResult<InsertedGroupPB> {
let group_data = GroupData::new(
group.id.clone(),
self.field.id.clone(),
group.name.clone(),
group.id.clone(),
group.visible,
);
let group_data = GroupData::new(group.id.clone(), self.field_id.clone(), group.visible);
self.group_by_id.insert(group.id.clone(), group_data);
let (index, group_data) = self.get_group(&group.id).unwrap();
let insert_group = InsertedGroupPB {
@ -232,7 +202,7 @@ where
configuration
.groups
.iter()
.map(|group| group.name.clone())
.map(|group| group.id.clone())
.collect::<Vec<String>>()
.join(",")
);
@ -268,22 +238,12 @@ where
) -> FlowyResult<Option<GroupChangesPB>> {
let GeneratedGroups {
no_status_group,
group_configs,
groups,
} = generated_groups;
let mut new_groups = vec![];
let mut filter_content_map = HashMap::new();
group_configs.into_iter().for_each(|generate_group| {
filter_content_map.insert(
generate_group.group.id.clone(),
generate_group.filter_content,
);
new_groups.push(generate_group.group);
});
let mut old_groups = self.setting.groups.clone();
// clear all the groups if grouping by a new field
if self.setting.field_id != self.field.id {
if self.setting.field_id != self.field_id {
old_groups.clear();
}
@ -292,7 +252,7 @@ where
mut all_groups,
new_groups,
deleted_groups,
} = merge_groups(no_status_group, old_groups, new_groups);
} = merge_groups(no_status_group, old_groups, groups);
let deleted_group_ids = deleted_groups
.into_iter()
@ -321,12 +281,10 @@ where
Some(pos) => {
let old_group = configuration.groups.get_mut(pos).unwrap();
// Take the old group setting
group.visible = old_group.visible;
if !is_changed {
is_changed = is_group_changed(group, old_group);
if group.visible != old_group.visible {
is_changed = true;
}
// Consider the the name of the `group_rev` as the newest.
old_group.name = group.name.clone();
group.visible = old_group.visible;
},
}
}
@ -335,31 +293,14 @@ where
// Update the memory cache of the groups
all_groups.into_iter().for_each(|group| {
let filter_content = filter_content_map
.get(&group.id)
.cloned()
.unwrap_or_else(|| "".to_owned());
let group = GroupData::new(
group.id,
self.field.id.clone(),
group.name,
filter_content,
group.visible,
);
let group = GroupData::new(group.id, self.field_id.clone(), group.visible);
self.group_by_id.insert(group.id.clone(), group);
});
let initial_groups = new_groups
.into_iter()
.flat_map(|group_rev| {
let filter_content = filter_content_map.get(&group_rev.id)?;
let group = GroupData::new(
group_rev.id,
self.field.id.clone(),
group_rev.name,
filter_content.clone(),
group_rev.visible,
);
let group = GroupData::new(group_rev.id, self.field_id.clone(), group_rev.visible);
Some(GroupPB::from(group))
})
.collect();
@ -385,14 +326,10 @@ where
if let Some(visible) = group_changeset.visible {
group.visible = visible;
}
if let Some(name) = &group_changeset.name {
group.name = name.clone();
}
})?;
if let Some(group) = update_group {
if let Some(group_data) = self.group_by_id.get_mut(&group.id) {
group_data.name = group.name.clone();
group_data.is_visible = group.visible;
};
}
@ -401,8 +338,8 @@ where
pub(crate) async fn get_all_cells(&self) -> Vec<RowSingleCellData> {
self
.reader
.get_configuration_cells(&self.view_id, &self.field.id)
.delegate
.get_configuration_cells(&self.view_id, &self.field_id)
.await
}
@ -423,10 +360,10 @@ where
let is_changed = mut_configuration_fn(configuration);
if is_changed {
let configuration = (*self.setting).clone();
let writer = self.writer.clone();
let delegate = self.delegate.clone();
let view_id = self.view_id.clone();
af_spawn(async move {
match writer.save_configuration(&view_id, configuration).await {
match delegate.save_configuration(&view_id, configuration).await {
Ok(_) => {},
Err(e) => {
tracing::error!("Save group configuration failed: {}", e);
@ -504,13 +441,6 @@ fn merge_groups(
merge_result
}
fn is_group_changed(new: &Group, old: &Group) -> bool {
if new.name != old.name {
return true;
}
false
}
struct MergeGroupResult {
// Contains the new groups and the updated groups
all_groups: Vec<Group>,
@ -545,13 +475,13 @@ mod tests {
exp_deleted_groups: Vec<&'a str>,
}
let new_group = |name: &str| Group::new(name.to_string(), name.to_string());
let new_group = |name: &str| Group::new(name.to_string());
let groups_from_strings =
|strings: Vec<&str>| strings.iter().map(|s| new_group(s)).collect::<Vec<Group>>();
let group_stringify = |groups: Vec<Group>| {
groups
.iter()
.map(|group| group.name.clone())
.map(|group| group.id.clone())
.collect::<Vec<String>>()
.join(",")
};

View File

@ -1,13 +1,13 @@
use std::marker::PhantomData;
use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cells, Row, RowDetail, RowId};
use futures::executor::block_on;
use serde::de::DeserializeOwned;
use serde::Serialize;
use flowy_error::FlowyResult;
use flowy_error::{FlowyError, FlowyResult};
use crate::entities::{
FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB,
@ -16,67 +16,43 @@ use crate::entities::{
use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser};
use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData};
use crate::services::group::action::{
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, GroupCustomize,
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController, GroupCustomize,
};
use crate::services::group::configuration::GroupContext;
use crate::services::group::configuration::GroupControllerContext;
use crate::services::group::entities::GroupData;
use crate::services::group::{GroupChangeset, GroupChangesets, GroupsBuilder, MoveGroupRowContext};
use crate::services::group::{GroupChangeset, GroupsBuilder, MoveGroupRowContext};
// use collab_database::views::Group;
pub trait GroupControllerDelegate: Send + Sync + 'static {
fn get_field(&self, field_id: &str) -> Option<Field>;
}
/// The [GroupController] trait defines the group actions, including create/delete/move items
/// For example, the group will insert a item if the one of the new [RowRevision]'s [CellRevision]s
/// content match the group filter.
/// [BaseGroupController] is a generic group controller that provides customized implementations
/// of the `GroupController` trait for different field types.
///
/// Different [FieldType] has a different controller that implements the [GroupController] trait.
/// If the [FieldType] doesn't implement its group controller, then the [DefaultGroupController] will
/// be used.
/// - `C`: represents the group configuration that impl [GroupConfigurationSerde]
/// - `G`: group generator, [GroupsBuilder]
/// - `P`: parser that impl [CellProtobufBlobParser] for the CellBytes
///
pub trait GroupController: GroupControllerOperation + Send + Sync {
/// Called when the type option of the [Field] was updated.
fn did_update_field_type_option(&mut self, field: &Field);
/// Called before the row was created.
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str);
}
#[async_trait]
pub trait GroupOperationInterceptor {
type GroupTypeOption: TypeOption;
async fn type_option_from_group_changeset(
&self,
_changeset: &GroupChangeset,
_type_option: &Self::GroupTypeOption,
_view_id: &str,
) -> Option<TypeOptionData> {
None
}
}
/// C: represents the group configuration that impl [GroupConfigurationSerde]
/// T: the type-option data deserializer that impl [TypeOptionDataDeserializer]
/// G: the group generator, [GroupsBuilder]
/// P: the parser that impl [CellProtobufBlobParser] for the CellBytes
pub struct BaseGroupController<C, T, G, P, I> {
/// See also: [DefaultGroupController] which contains the most basic implementation of
/// `GroupController` that only has one group.
pub struct BaseGroupController<C, G, P> {
pub grouping_field_id: String,
pub type_option: T,
pub context: GroupContext<C>,
pub context: GroupControllerContext<C>,
group_builder_phantom: PhantomData<G>,
cell_parser_phantom: PhantomData<P>,
pub operation_interceptor: I,
pub delegate: Arc<dyn GroupControllerDelegate>,
}
impl<C, T, G, P, I> BaseGroupController<C, T, G, P, I>
impl<C, T, G, P> BaseGroupController<C, G, P>
where
C: Serialize + DeserializeOwned,
T: TypeOption + From<TypeOptionData> + Send + Sync,
G: GroupsBuilder<Context = GroupContext<C>, GroupTypeOption = T>,
I: GroupOperationInterceptor<GroupTypeOption = T> + Send + Sync,
T: TypeOption + Send + Sync,
G: GroupsBuilder<Context = GroupControllerContext<C>, GroupTypeOption = T>,
{
pub async fn new(
grouping_field: &Field,
mut configuration: GroupContext<C>,
operation_interceptor: I,
mut configuration: GroupControllerContext<C>,
delegate: Arc<dyn GroupControllerDelegate>,
) -> FlowyResult<Self> {
let field_type = FieldType::from(grouping_field.field_type);
let type_option = grouping_field
@ -89,14 +65,20 @@ where
Ok(Self {
grouping_field_id: grouping_field.id.clone(),
type_option,
context: configuration,
group_builder_phantom: PhantomData,
cell_parser_phantom: PhantomData,
operation_interceptor,
delegate,
})
}
pub fn get_grouping_field_type_option(&self) -> Option<T> {
self
.delegate
.get_field(&self.grouping_field_id)
.and_then(|field| field.get_type_option::<T>(FieldType::from(field.field_type)))
}
fn update_no_status_group(
&mut self,
row_detail: &RowDetail,
@ -169,14 +151,12 @@ where
}
}
#[async_trait]
impl<C, T, G, P, I> GroupControllerOperation for BaseGroupController<C, T, G, P, I>
impl<C, T, G, P> GroupController for BaseGroupController<C, G, P>
where
P: CellProtobufBlobParser<Object = <T as TypeOption>::CellProtobufType>,
C: Serialize + DeserializeOwned + Sync + Send,
T: TypeOption + From<TypeOptionData> + Send + Sync,
G: GroupsBuilder<Context = GroupContext<C>, GroupTypeOption = T>,
I: GroupOperationInterceptor<GroupTypeOption = T> + Send + Sync,
T: TypeOption + Send + Sync,
G: GroupsBuilder<Context = GroupControllerContext<C>, GroupTypeOption = T>,
Self: GroupCustomize<GroupTypeOption = T>,
{
fn field_id(&self) -> &str {
@ -204,7 +184,7 @@ where
let mut grouped_rows: Vec<GroupedRow> = vec![];
let cell_data = <T as TypeOption>::CellData::from(&cell);
for group in self.context.groups() {
if self.can_group(&group.filter_content, &cell_data) {
if self.can_group(&group.id, &cell_data) {
grouped_rows.push(GroupedRow {
row_detail: (*row_detail).clone(),
group_id: group.id.clone(),
@ -236,7 +216,7 @@ where
&mut self,
name: String,
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
self.generate_new_group(name)
<Self as GroupCustomize>::create_group(self, name)
}
fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> {
@ -260,7 +240,7 @@ where
let mut suitable_group_ids = vec![];
for group in self.get_all_groups() {
if self.can_group(&group.filter_content, &cell_data) {
if self.can_group(&group.id, &cell_data) {
suitable_group_ids.push(group.id.clone());
let changeset = GroupRowsNotificationPB::insert(
group.id.clone(),
@ -384,7 +364,7 @@ where
let cell_bytes = get_cell_protobuf(&cell, context.field, None);
let cell_data = cell_bytes.parser::<P>()?;
result.deleted_group = self.delete_group_when_move_row(&context.row_detail.row, &cell_data);
result.row_changesets = self.move_row(&cell_data, context);
result.row_changesets = self.move_row(context);
} else {
tracing::warn!("Unexpected moving group row, changes should not be empty");
}
@ -409,32 +389,30 @@ where
.iter()
.map(|row| row.row.id.clone())
.collect();
let type_option_data = self.delete_group_custom(group_id)?;
let type_option_data = <Self as GroupCustomize>::delete_group(self, group_id)?;
Ok((row_ids, type_option_data))
},
None => Ok((vec![], None)),
}
}
async fn apply_group_changeset(
fn apply_group_changeset(
&mut self,
changeset: &GroupChangesets,
changeset: &[GroupChangeset],
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> {
for group_changeset in changeset.changesets.iter() {
for group_changeset in changeset.iter() {
self.context.update_group(group_changeset)?;
}
let mut type_option_data = TypeOptionData::new();
for group_changeset in changeset.changesets.iter() {
if let Some(new_type_option_data) = self
.operation_interceptor
.type_option_from_group_changeset(group_changeset, &self.type_option, &self.context.view_id)
.await
{
type_option_data.extend(new_type_option_data);
}
let mut type_option = self.get_grouping_field_type_option().ok_or_else(|| {
FlowyError::internal().with_context("Failed to get grouping field type option")
})?;
for group_changeset in changeset.iter() {
self.update_type_option_when_update_group(group_changeset, &mut type_option);
}
let updated_groups = changeset
.changesets
.iter()
.filter_map(|changeset| {
self
@ -442,7 +420,12 @@ where
.map(|(_, group)| GroupPB::from(group))
})
.collect::<Vec<_>>();
Ok((updated_groups, type_option_data))
Ok((updated_groups, type_option.into()))
}
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
<Self as GroupCustomize>::will_create_row(self, cells, field, group_id);
}
}

View File

@ -10,11 +10,10 @@ use crate::services::field::{
CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK,
};
use crate::services::group::action::GroupCustomize;
use crate::services::group::configuration::GroupContext;
use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::configuration::GroupControllerContext;
use crate::services::group::controller::BaseGroupController;
use crate::services::group::{
move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupOperationInterceptor,
GroupsBuilder, MoveGroupRowContext,
move_group_row, GeneratedGroups, Group, GroupsBuilder, MoveGroupRowContext,
};
#[derive(Default, Serialize, Deserialize)]
@ -22,16 +21,10 @@ pub struct CheckboxGroupConfiguration {
pub hide_empty: bool,
}
pub type CheckboxGroupController = BaseGroupController<
CheckboxGroupConfiguration,
CheckboxTypeOption,
CheckboxGroupBuilder,
CheckboxCellDataParser,
CheckboxGroupOperationInterceptorImpl,
>;
pub type CheckboxGroupContext = GroupContext<CheckboxGroupConfiguration>;
pub type CheckboxGroupController =
BaseGroupController<CheckboxGroupConfiguration, CheckboxGroupBuilder, CheckboxCellDataParser>;
pub type CheckboxGroupControllerContext = GroupControllerContext<CheckboxGroupConfiguration>;
impl GroupCustomize for CheckboxGroupController {
type GroupTypeOption = CheckboxTypeOption;
fn placeholder_cell(&self) -> Option<Cell> {
@ -126,11 +119,7 @@ impl GroupCustomize for CheckboxGroupController {
(None, changesets)
}
fn move_row(
&mut self,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
mut context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB> {
fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB> {
let mut group_changeset = vec![];
self.context.iter_mut_groups(|group| {
if let Some(changeset) = move_group_row(group, &mut context) {
@ -140,17 +129,11 @@ impl GroupCustomize for CheckboxGroupController {
group_changeset
}
fn delete_group_custom(&mut self, _group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
fn delete_group(&mut self, _group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
Ok(None)
}
}
impl GroupController for CheckboxGroupController {
fn did_update_field_type_option(&mut self, _field: &Field) {
// Do nothing
}
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) {
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, group)) => {
@ -165,7 +148,7 @@ impl GroupController for CheckboxGroupController {
pub struct CheckboxGroupBuilder();
#[async_trait]
impl GroupsBuilder for CheckboxGroupBuilder {
type Context = CheckboxGroupContext;
type Context = CheckboxGroupControllerContext;
type GroupTypeOption = CheckboxTypeOption;
async fn build(
@ -173,26 +156,12 @@ impl GroupsBuilder for CheckboxGroupBuilder {
_context: &Self::Context,
_type_option: &Self::GroupTypeOption,
) -> GeneratedGroups {
let check_group = GeneratedGroupConfig {
group: Group::new(CHECK.to_string(), "".to_string()),
filter_content: CHECK.to_string(),
};
let uncheck_group = GeneratedGroupConfig {
group: Group::new(UNCHECK.to_string(), "".to_string()),
filter_content: UNCHECK.to_string(),
};
let check_group = Group::new(CHECK.to_string());
let uncheck_group = Group::new(UNCHECK.to_string());
GeneratedGroups {
no_status_group: None,
group_configs: vec![check_group, uncheck_group],
groups: vec![check_group, uncheck_group],
}
}
}
pub struct CheckboxGroupOperationInterceptorImpl {}
#[async_trait]
impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl {
type GroupTypeOption = CheckboxTypeOption;
}

View File

@ -1,7 +1,5 @@
use std::format;
use async_trait::async_trait;
use chrono::{DateTime, Datelike, Days, Duration, Local, NaiveDate, NaiveDateTime};
use chrono::{DateTime, Datelike, Days, Duration, Local, NaiveDateTime};
use collab_database::database::timestamp;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
@ -16,28 +14,24 @@ use crate::entities::{
use crate::services::cell::insert_date_cell;
use crate::services::field::{DateCellData, DateCellDataParser, DateTypeOption, TypeOption};
use crate::services::group::action::GroupCustomize;
use crate::services::group::configuration::GroupContext;
use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::configuration::GroupControllerContext;
use crate::services::group::controller::BaseGroupController;
use crate::services::group::{
make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group,
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
make_no_status_group, move_group_row, GeneratedGroups, Group, GroupsBuilder, MoveGroupRowContext,
};
pub trait GroupConfigurationContentSerde: Sized + Send + Sync {
fn from_json(s: &str) -> Result<Self, serde_json::Error>;
fn to_json(&self) -> Result<String, serde_json::Error>;
}
#[derive(Default, Serialize, Deserialize)]
pub struct DateGroupConfiguration {
pub hide_empty: bool,
pub condition: DateCondition,
}
impl GroupConfigurationContentSerde for DateGroupConfiguration {
impl DateGroupConfiguration {
fn from_json(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
#[allow(dead_code)]
fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
@ -54,15 +48,10 @@ pub enum DateCondition {
Year = 4,
}
pub type DateGroupController = BaseGroupController<
DateGroupConfiguration,
DateTypeOption,
DateGroupBuilder,
DateCellDataParser,
DateGroupOperationInterceptorImpl,
>;
pub type DateGroupController =
BaseGroupController<DateGroupConfiguration, DateGroupBuilder, DateCellDataParser>;
pub type DateGroupContext = GroupContext<DateGroupConfiguration>;
pub type DateGroupControllerContext = GroupControllerContext<DateGroupConfiguration>;
impl GroupCustomize for DateGroupController {
type GroupTypeOption = DateTypeOption;
@ -80,7 +69,7 @@ impl GroupCustomize for DateGroupController {
content: &str,
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
) -> bool {
content == group_id(cell_data, &self.context.get_setting_content())
content == get_date_group_id(cell_data, &self.context.get_setting_content())
}
fn create_or_delete_group_when_cell_changed(
@ -93,7 +82,7 @@ impl GroupCustomize for DateGroupController {
let mut inserted_group = None;
if self
.context
.get_group(&group_id(&_cell_data.into(), &setting_content))
.get_group(&get_date_group_id(&_cell_data.into(), &setting_content))
.is_none()
{
let group = make_group_from_date_cell(&_cell_data.into(), &setting_content);
@ -106,7 +95,7 @@ impl GroupCustomize for DateGroupController {
let deleted_group = match _old_cell_data.and_then(|old_cell_data| {
self
.context
.get_group(&group_id(&old_cell_data.into(), &setting_content))
.get_group(&get_date_group_id(&old_cell_data.into(), &setting_content))
}) {
None => None,
Some((_, group)) => {
@ -138,7 +127,7 @@ impl GroupCustomize for DateGroupController {
let setting_content = self.context.get_setting_content();
self.context.iter_mut_status_groups(|group| {
let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
if group.id == group_id(&cell_data.into(), &setting_content) {
if group.id == get_date_group_id(&cell_data.into(), &setting_content) {
if !group.contains_row(&row_detail.row.id) {
changeset
.inserted_rows
@ -180,7 +169,7 @@ impl GroupCustomize for DateGroupController {
let setting_content = self.context.get_setting_content();
let deleted_group = match self
.context
.get_group(&group_id(cell_data, &setting_content))
.get_group(&get_date_group_id(cell_data, &setting_content))
{
Some((_, group)) if group.rows.len() == 1 => Some(group.clone()),
_ => None,
@ -194,11 +183,7 @@ impl GroupCustomize for DateGroupController {
(deleted_group, changesets)
}
fn move_row(
&mut self,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
mut context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB> {
fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB> {
let mut group_changeset = vec![];
self.context.iter_mut_groups(|group| {
if let Some(changeset) = move_group_row(group, &mut context) {
@ -211,13 +196,13 @@ impl GroupCustomize for DateGroupController {
fn delete_group_when_move_row(
&mut self,
_row: &Row,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
) -> Option<GroupPB> {
let mut deleted_group = None;
let setting_content = self.context.get_setting_content();
if let Some((_, group)) = self
.context
.get_group(&group_id(&_cell_data.into(), &setting_content))
.get_group(&get_date_group_id(&cell_data.into(), &setting_content))
{
if group.rows.len() == 1 {
deleted_group = Some(GroupPB::from(group.clone()));
@ -229,16 +214,12 @@ impl GroupCustomize for DateGroupController {
deleted_group
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
self.context.delete_group(group_id)?;
Ok(None)
}
}
impl GroupController for DateGroupController {
fn did_update_field_type_option(&mut self, _field: &Field) {}
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) {
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, _)) => {
@ -253,7 +234,7 @@ impl GroupController for DateGroupController {
pub struct DateGroupBuilder();
#[async_trait]
impl GroupsBuilder for DateGroupBuilder {
type Context = DateGroupContext;
type Context = DateGroupControllerContext;
type GroupTypeOption = DateTypeOption;
async fn build(
@ -265,39 +246,31 @@ impl GroupsBuilder for DateGroupBuilder {
let cells = context.get_all_cells().await;
// Generate the groups
let mut group_configs: Vec<GeneratedGroupConfig> = cells
let mut groups: Vec<Group> = cells
.into_iter()
.flat_map(|value| value.into_date_field_cell_data())
.filter(|cell| cell.timestamp.is_some())
.map(|cell| {
let group = make_group_from_date_cell(&cell, &context.get_setting_content());
GeneratedGroupConfig {
filter_content: group.id.clone(),
group,
}
})
.map(|cell| make_group_from_date_cell(&cell, &context.get_setting_content()))
.collect();
group_configs.sort_by(|a, b| a.filter_content.cmp(&b.filter_content));
groups.sort_by(|a, b| a.id.cmp(&b.id));
let no_status_group = Some(make_no_status_group(field));
GeneratedGroups {
no_status_group,
group_configs,
groups,
}
}
}
fn make_group_from_date_cell(cell_data: &DateCellData, setting_content: &str) -> Group {
let group_id = group_id(cell_data, setting_content);
Group::new(
group_id.clone(),
group_name_from_id(&group_id, setting_content),
)
let group_id = get_date_group_id(cell_data, setting_content);
Group::new(group_id)
}
const GROUP_ID_DATE_FORMAT: &str = "%Y/%m/%d";
fn group_id(cell_data: &DateCellData, setting_content: &str) -> String {
fn get_date_group_id(cell_data: &DateCellData, setting_content: &str) -> String {
let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default();
let date_time = date_time_from_timestamp(cell_data.timestamp);
@ -354,63 +327,6 @@ fn group_id(cell_data: &DateCellData, setting_content: &str) -> String {
date.to_string()
}
fn group_name_from_id(group_id: &str, setting_content: &str) -> String {
let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default();
let date = NaiveDate::parse_from_str(group_id, GROUP_ID_DATE_FORMAT).unwrap();
let tmp;
match config.condition {
DateCondition::Day => {
tmp = format!("{} {}, {}", date.format("%b"), date.day(), date.year(),);
tmp
},
DateCondition::Week => {
let begin_of_week = date
.checked_sub_days(Days::new(date.weekday().num_days_from_monday() as u64))
.unwrap()
.format("%d");
let end_of_week = date
.checked_add_days(Days::new(6 - date.weekday().num_days_from_monday() as u64))
.unwrap()
.format("%d");
tmp = format!(
"Week of {} {}-{} {}",
date.format("%b"),
begin_of_week,
end_of_week,
date.year()
);
tmp
},
DateCondition::Month => {
tmp = format!("{} {}", date.format("%b"), date.year(),);
tmp
},
DateCondition::Year => date.year().to_string(),
DateCondition::Relative => {
let now = date_time_from_timestamp(Some(timestamp()));
let diff = date.signed_duration_since(now.date_naive());
let result = match diff.num_days() {
0 => "Today",
-1 => "Yesterday",
1 => "Tomorrow",
-7 => "Last 7 days",
2 => "Next 7 days",
-30 => "Last 30 days",
8 => "Next 30 days",
_ => {
tmp = format!("{} {}", date.format("%b"), date.year(),);
&tmp
},
};
result.to_string()
},
}
}
fn date_time_from_timestamp(timestamp: Option<i64>) -> DateTime<Local> {
match timestamp {
Some(timestamp) => {
@ -423,24 +339,14 @@ fn date_time_from_timestamp(timestamp: Option<i64>) -> DateTime<Local> {
}
}
pub struct DateGroupOperationInterceptorImpl {}
#[async_trait]
impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl {
type GroupTypeOption = DateTypeOption;
}
#[cfg(test)]
mod tests {
use std::vec;
use chrono::{offset, Days, Duration, NaiveDateTime};
use crate::services::{
field::{date_type_option::DateTypeOption, DateCellData},
group::controller_impls::date_controller::{
group_id, group_name_from_id, GROUP_ID_DATE_FORMAT,
},
use crate::services::field::date_type_option::DateTypeOption;
use crate::services::field::DateCellData;
use crate::services::group::controller_impls::date_controller::{
get_date_group_id, GROUP_ID_DATE_FORMAT,
};
#[test]
@ -449,7 +355,6 @@ mod tests {
cell_data: DateCellData,
setting_content: String,
exp_group_id: String,
exp_group_name: String,
}
let mar_14_2022 = NaiveDateTime::from_timestamp_opt(1647251762, 0).unwrap();
@ -471,7 +376,6 @@ mod tests {
cell_data: mar_14_2022_cd.clone(),
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
exp_group_id: "2022/03/01".to_string(),
exp_group_name: "Mar 2022".to_string(),
},
GroupIDTest {
cell_data: DateCellData {
@ -481,7 +385,6 @@ mod tests {
},
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
exp_group_id: today.format(GROUP_ID_DATE_FORMAT).to_string(),
exp_group_name: "Today".to_string(),
},
GroupIDTest {
cell_data: DateCellData {
@ -495,13 +398,11 @@ mod tests {
.unwrap()
.format(GROUP_ID_DATE_FORMAT)
.to_string(),
exp_group_name: "Last 7 days".to_string(),
},
GroupIDTest {
cell_data: mar_14_2022_cd.clone(),
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
exp_group_id: "2022/03/14".to_string(),
exp_group_name: "Mar 14, 2022".to_string(),
},
GroupIDTest {
cell_data: DateCellData {
@ -516,19 +417,16 @@ mod tests {
},
setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(),
exp_group_id: "2022/03/14".to_string(),
exp_group_name: "Week of Mar 14-20 2022".to_string(),
},
GroupIDTest {
cell_data: mar_14_2022_cd.clone(),
setting_content: r#"{"condition": 3, "hide_empty": false}"#.to_string(),
exp_group_id: "2022/03/01".to_string(),
exp_group_name: "Mar 2022".to_string(),
},
GroupIDTest {
cell_data: mar_14_2022_cd,
setting_content: r#"{"condition": 4, "hide_empty": false}"#.to_string(),
exp_group_id: "2022/01/01".to_string(),
exp_group_name: "2022".to_string(),
},
GroupIDTest {
cell_data: DateCellData {
@ -538,7 +436,6 @@ mod tests {
},
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
exp_group_id: "2023/06/02".to_string(),
exp_group_name: "".to_string(),
},
GroupIDTest {
cell_data: DateCellData {
@ -548,18 +445,12 @@ mod tests {
},
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
exp_group_id: "2023/06/03".to_string(),
exp_group_name: "".to_string(),
},
];
for (i, test) in tests.iter().enumerate() {
let group_id = group_id(&test.cell_data, &test.setting_content);
let group_id = get_date_group_id(&test.cell_data, &test.setting_content);
assert_eq!(test.exp_group_id, group_id, "test {}", i);
if !test.exp_group_name.is_empty() {
let group_name = group_name_from_id(&group_id, &test.setting_content);
assert_eq!(test.exp_group_name, group_name, "test {}", i);
}
}
}
}

View File

@ -8,9 +8,9 @@ use crate::entities::{
GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB,
};
use crate::services::group::action::{
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation,
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController,
};
use crate::services::group::{GroupChangesets, GroupController, GroupData, MoveGroupRowContext};
use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext};
/// A [DefaultGroupController] is used to handle the group actions for the [FieldType] that doesn't
/// implement its own group controller. The default group controller only contains one group, which
@ -25,13 +25,7 @@ const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController";
impl DefaultGroupController {
pub fn new(field: &Field) -> Self {
let group = GroupData::new(
DEFAULT_GROUP_CONTROLLER.to_owned(),
field.id.clone(),
"".to_owned(),
"".to_owned(),
true,
);
let group = GroupData::new(DEFAULT_GROUP_CONTROLLER.to_owned(), field.id.clone(), true);
Self {
field_id: field.id.clone(),
group,
@ -40,7 +34,7 @@ impl DefaultGroupController {
}
#[async_trait]
impl GroupControllerOperation for DefaultGroupController {
impl GroupController for DefaultGroupController {
fn field_id(&self) -> &str {
&self.field_id
}
@ -131,18 +125,12 @@ impl GroupControllerOperation for DefaultGroupController {
Ok((vec![], None))
}
async fn apply_group_changeset(
fn apply_group_changeset(
&mut self,
_changeset: &GroupChangesets,
_changeset: &[GroupChangeset],
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> {
Ok((Vec::new(), TypeOptionData::default()))
}
}
impl GroupController for DefaultGroupController {
fn did_update_field_type_option(&mut self, _field: &Field) {
// Do nothing
}
fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {}
fn will_create_row(&self, _cells: &mut Cells, _field: &Field, _group_id: &str) {}
}

View File

@ -1,7 +1,7 @@
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use flowy_error::FlowyResult;
use flowy_error::{FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB};
@ -11,11 +11,11 @@ use crate::services::field::{
TypeOption,
};
use crate::services::group::action::GroupCustomize;
use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::controller::BaseGroupController;
use crate::services::group::{
add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group,
move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset, GroupContext,
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset,
GroupControllerContext, GroupsBuilder, MoveGroupRowContext,
};
#[derive(Default, Serialize, Deserialize)]
@ -23,14 +23,12 @@ pub struct MultiSelectGroupConfiguration {
pub hide_empty: bool,
}
pub type MultiSelectOptionGroupContext = GroupContext<MultiSelectGroupConfiguration>;
pub type MultiSelectGroupControllerContext = GroupControllerContext<MultiSelectGroupConfiguration>;
// MultiSelect
pub type MultiSelectGroupController = BaseGroupController<
MultiSelectGroupConfiguration,
MultiSelectTypeOption,
MultiSelectGroupBuilder,
SelectOptionCellDataParser,
MultiSelectGroupOperationInterceptorImpl,
>;
impl GroupCustomize for MultiSelectGroupController {
@ -80,11 +78,7 @@ impl GroupCustomize for MultiSelectGroupController {
(None, changesets)
}
fn move_row(
&mut self,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
mut context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB> {
fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB> {
let mut group_changeset = vec![];
self.context.iter_mut_groups(|group| {
if let Some(changeset) = move_group_row(group, &mut context) {
@ -94,83 +88,44 @@ impl GroupCustomize for MultiSelectGroupController {
group_changeset
}
fn generate_new_group(
fn create_group(
&mut self,
name: String,
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
let mut new_type_option = self.type_option.clone();
let new_select_option = self.type_option.create_option(&name);
let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| {
FlowyError::internal().with_context("Failed to get grouping field type option")
})?;
let new_select_option = new_type_option.create_option(&name);
new_type_option.insert_option(new_select_option.clone());
let new_group = Group::new(new_select_option.id, new_select_option.name);
let new_group = Group::new(new_select_option.id);
let inserted_group_pb = self.context.add_new_group(new_group)?;
Ok((Some(new_type_option.into()), Some(inserted_group_pb)))
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
if let Some(option_index) = self
.type_option
fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| {
FlowyError::internal().with_context("Failed to get grouping field type option")
})?;
if let Some(option_index) = new_type_option
.options
.iter()
.position(|option| option.id == group_id)
{
// Remove the option if the group is found
let mut new_type_option = self.type_option.clone();
new_type_option.options.remove(option_index);
Ok(Some(new_type_option.into()))
} else {
Ok(None)
}
}
}
impl GroupController for MultiSelectGroupController {
fn did_update_field_type_option(&mut self, _field: &Field) {}
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, group)) => {
let cell = insert_select_option_cell(vec![group.id.clone()], field);
cells.insert(field.id.clone(), cell);
},
}
}
}
pub struct MultiSelectGroupBuilder;
#[async_trait]
impl GroupsBuilder for MultiSelectGroupBuilder {
type Context = MultiSelectOptionGroupContext;
type GroupTypeOption = MultiSelectTypeOption;
async fn build(
field: &Field,
_context: &Self::Context,
type_option: &Self::GroupTypeOption,
) -> GeneratedGroups {
let group_configs = generate_select_option_groups(&field.id, &type_option.options);
GeneratedGroups {
no_status_group: Some(make_no_status_group(field)),
group_configs,
}
}
}
pub struct MultiSelectGroupOperationInterceptorImpl;
#[async_trait]
impl GroupOperationInterceptor for MultiSelectGroupOperationInterceptorImpl {
type GroupTypeOption = MultiSelectTypeOption;
#[tracing::instrument(level = "trace", skip_all)]
async fn type_option_from_group_changeset(
&self,
fn update_type_option_when_update_group(
&mut self,
changeset: &GroupChangeset,
type_option: &Self::GroupTypeOption,
_view_id: &str,
) -> Option<TypeOptionData> {
type_option: &mut Self::GroupTypeOption,
) {
if let Some(name) = &changeset.name {
let mut new_type_option = type_option.clone();
let select_option = type_option
@ -184,9 +139,36 @@ impl GroupOperationInterceptor for MultiSelectGroupOperationInterceptorImpl {
..select_option.to_owned()
};
new_type_option.insert_option(new_select_option);
return Some(new_type_option.into());
}
}
None
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_index, group)) => {
let cell = insert_select_option_cell(vec![group.id.clone()], field);
cells.insert(field.id.clone(), cell);
},
}
}
}
pub struct MultiSelectGroupBuilder;
#[async_trait]
impl GroupsBuilder for MultiSelectGroupBuilder {
type Context = MultiSelectGroupControllerContext;
type GroupTypeOption = MultiSelectTypeOption;
async fn build(
field: &Field,
_context: &Self::Context,
type_option: &Self::GroupTypeOption,
) -> GeneratedGroups {
let groups = generate_select_option_groups(&field.id, &type_option.options);
GeneratedGroups {
no_status_group: Some(make_no_status_group(field)),
groups,
}
}
}

View File

@ -1,7 +1,7 @@
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
use flowy_error::FlowyResult;
use flowy_error::{FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB};
@ -11,12 +11,11 @@ use crate::services::field::{
TypeOption,
};
use crate::services::group::action::GroupCustomize;
use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::controller::BaseGroupController;
use crate::services::group::controller_impls::select_option_controller::util::*;
use crate::services::group::entities::GroupData;
use crate::services::group::{
make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupContext,
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupControllerContext,
GroupsBuilder, MoveGroupRowContext,
};
#[derive(Default, Serialize, Deserialize)]
@ -24,19 +23,19 @@ pub struct SingleSelectGroupConfiguration {
pub hide_empty: bool,
}
pub type SingleSelectOptionGroupContext = GroupContext<SingleSelectGroupConfiguration>;
pub type SingleSelectGroupControllerContext =
GroupControllerContext<SingleSelectGroupConfiguration>;
// SingleSelect
pub type SingleSelectGroupController = BaseGroupController<
SingleSelectGroupConfiguration,
SingleSelectTypeOption,
SingleSelectGroupBuilder,
SelectOptionCellDataParser,
SingleSelectGroupOperationInterceptorImpl,
>;
impl GroupCustomize for SingleSelectGroupController {
type GroupTypeOption = SingleSelectTypeOption;
fn can_group(
&self,
content: &str,
@ -81,11 +80,7 @@ impl GroupCustomize for SingleSelectGroupController {
(None, changesets)
}
fn move_row(
&mut self,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
mut context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB> {
fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB> {
let mut group_changeset = vec![];
self.context.iter_mut_groups(|group| {
if let Some(changeset) = move_group_row(group, &mut context) {
@ -95,85 +90,44 @@ impl GroupCustomize for SingleSelectGroupController {
group_changeset
}
fn generate_new_group(
fn create_group(
&mut self,
name: String,
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
let mut new_type_option = self.type_option.clone();
let new_select_option = self.type_option.create_option(&name);
let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| {
FlowyError::internal().with_context("Failed to get grouping field type option")
})?;
let new_select_option = new_type_option.create_option(&name);
new_type_option.insert_option(new_select_option.clone());
let new_group = Group::new(new_select_option.id, new_select_option.name);
let new_group = Group::new(new_select_option.id);
let inserted_group_pb = self.context.add_new_group(new_group)?;
Ok((Some(new_type_option.into()), Some(inserted_group_pb)))
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
if let Some(option_index) = self
.type_option
fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| {
FlowyError::internal().with_context("Failed to get grouping field type option")
})?;
if let Some(option_index) = new_type_option
.options
.iter()
.position(|option| option.id == group_id)
{
// Remove the option if the group is found
let mut new_type_option = self.type_option.clone();
new_type_option.options.remove(option_index);
Ok(Some(new_type_option.into()))
} else {
// Return None if no matching group is found
Ok(None)
}
}
}
impl GroupController for SingleSelectGroupController {
fn did_update_field_type_option(&mut self, _field: &Field) {}
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) {
let group: Option<&mut GroupData> = self.context.get_mut_group(group_id);
match group {
None => {},
Some(group) => {
let cell = insert_select_option_cell(vec![group.id.clone()], field);
cells.insert(field.id.clone(), cell);
},
}
}
}
pub struct SingleSelectGroupBuilder();
#[async_trait]
impl GroupsBuilder for SingleSelectGroupBuilder {
type Context = SingleSelectOptionGroupContext;
type GroupTypeOption = SingleSelectTypeOption;
async fn build(
field: &Field,
_context: &Self::Context,
type_option: &Self::GroupTypeOption,
) -> GeneratedGroups {
let group_configs = generate_select_option_groups(&field.id, &type_option.options);
GeneratedGroups {
no_status_group: Some(make_no_status_group(field)),
group_configs,
}
}
}
pub struct SingleSelectGroupOperationInterceptorImpl;
#[async_trait]
impl GroupOperationInterceptor for SingleSelectGroupOperationInterceptorImpl {
type GroupTypeOption = SingleSelectTypeOption;
#[tracing::instrument(level = "trace", skip_all)]
async fn type_option_from_group_changeset(
&self,
fn update_type_option_when_update_group(
&mut self,
changeset: &GroupChangeset,
type_option: &Self::GroupTypeOption,
_view_id: &str,
) -> Option<TypeOptionData> {
type_option: &mut Self::GroupTypeOption,
) {
if let Some(name) = &changeset.name {
let mut new_type_option = type_option.clone();
let select_option = type_option
@ -187,9 +141,35 @@ impl GroupOperationInterceptor for SingleSelectGroupOperationInterceptorImpl {
..select_option.to_owned()
};
new_type_option.insert_option(new_select_option);
return Some(new_type_option.into());
}
}
None
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_index, group)) => {
let cell = insert_select_option_cell(vec![group.id.clone()], field);
cells.insert(field.id.clone(), cell);
},
}
}
}
pub struct SingleSelectGroupBuilder();
#[async_trait]
impl GroupsBuilder for SingleSelectGroupBuilder {
type Context = SingleSelectGroupControllerContext;
type GroupTypeOption = SingleSelectTypeOption;
async fn build(
field: &Field,
_context: &Self::Context,
type_option: &Self::GroupTypeOption,
) -> GeneratedGroups {
let groups = generate_select_option_groups(&field.id, &type_option.options);
GeneratedGroups {
no_status_group: Some(make_no_status_group(field)),
groups,
}
}
}

View File

@ -9,7 +9,7 @@ use crate::services::cell::{
insert_checkbox_cell, insert_date_cell, insert_select_option_cell, insert_url_cell,
};
use crate::services::field::{SelectOption, SelectOptionIds, CHECK};
use crate::services::group::{GeneratedGroupConfig, Group, GroupData, MoveGroupRowContext};
use crate::services::group::{Group, GroupData, MoveGroupRowContext};
pub fn add_or_remove_select_option_row(
group: &mut GroupData,
@ -186,16 +186,10 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option<Cell> {
}
}
pub fn generate_select_option_groups(
_field_id: &str,
options: &[SelectOption],
) -> Vec<GeneratedGroupConfig> {
pub fn generate_select_option_groups(_field_id: &str, options: &[SelectOption]) -> Vec<Group> {
let groups = options
.iter()
.map(|option| GeneratedGroupConfig {
group: Group::new(option.id.clone(), option.name.clone()),
filter_content: option.id.clone(),
})
.map(|option| Group::new(option.id.clone()))
.collect();
groups

View File

@ -1,5 +1,3 @@
use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
@ -13,11 +11,10 @@ use crate::entities::{
use crate::services::cell::insert_url_cell;
use crate::services::field::{TypeOption, URLCellData, URLCellDataParser, URLTypeOption};
use crate::services::group::action::GroupCustomize;
use crate::services::group::configuration::GroupContext;
use crate::services::group::controller::{BaseGroupController, GroupController};
use crate::services::group::configuration::GroupControllerContext;
use crate::services::group::controller::BaseGroupController;
use crate::services::group::{
make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group,
GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, MoveGroupRowContext,
make_no_status_group, move_group_row, GeneratedGroups, Group, GroupsBuilder, MoveGroupRowContext,
};
#[derive(Default, Serialize, Deserialize)]
@ -25,15 +22,10 @@ pub struct URLGroupConfiguration {
pub hide_empty: bool,
}
pub type URLGroupController = BaseGroupController<
URLGroupConfiguration,
URLTypeOption,
URLGroupGenerator,
URLCellDataParser,
URLGroupOperationInterceptorImpl,
>;
pub type URLGroupController =
BaseGroupController<URLGroupConfiguration, URLGroupGenerator, URLCellDataParser>;
pub type URLGroupContext = GroupContext<URLGroupConfiguration>;
pub type URLGroupControllerContext = GroupControllerContext<URLGroupConfiguration>;
impl GroupCustomize for URLGroupController {
type GroupTypeOption = URLTypeOption;
@ -64,7 +56,7 @@ impl GroupCustomize for URLGroupController {
let mut inserted_group = None;
if self.context.get_group(&_cell_data.url).is_none() {
let cell_data: URLCellData = _cell_data.clone().into();
let group = make_group_from_url_cell(&cell_data);
let group = Group::new(cell_data.data);
let mut new_group = self.context.add_new_group(group)?;
new_group.group.rows.push(RowMetaPB::from(_row_detail));
inserted_group = Some(new_group);
@ -155,11 +147,7 @@ impl GroupCustomize for URLGroupController {
(deleted_group, changesets)
}
fn move_row(
&mut self,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
mut context: MoveGroupRowContext,
) -> Vec<GroupRowsNotificationPB> {
fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB> {
let mut group_changeset = vec![];
self.context.iter_mut_groups(|group| {
if let Some(changeset) = move_group_row(group, &mut context) {
@ -168,13 +156,14 @@ impl GroupCustomize for URLGroupController {
});
group_changeset
}
fn delete_group_when_move_row(
&mut self,
_row: &Row,
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType,
) -> Option<GroupPB> {
let mut deleted_group = None;
if let Some((_, group)) = self.context.get_group(&_cell_data.content) {
if let Some((_index, group)) = self.context.get_group(&cell_data.content) {
if group.rows.len() == 1 {
deleted_group = Some(GroupPB::from(group.clone()));
}
@ -185,16 +174,12 @@ impl GroupCustomize for URLGroupController {
deleted_group
}
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> {
self.context.delete_group(group_id)?;
Ok(None)
}
}
impl GroupController for URLGroupController {
fn did_update_field_type_option(&mut self, _field: &Field) {}
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) {
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {
match self.context.get_group(group_id) {
None => tracing::warn!("Can not find the group: {}", group_id),
Some((_, group)) => {
@ -208,7 +193,7 @@ impl GroupController for URLGroupController {
pub struct URLGroupGenerator();
#[async_trait]
impl GroupsBuilder for URLGroupGenerator {
type Context = URLGroupContext;
type Context = URLGroupControllerContext;
type GroupTypeOption = URLTypeOption;
async fn build(
@ -220,36 +205,18 @@ impl GroupsBuilder for URLGroupGenerator {
let cells = context.get_all_cells().await;
// Generate the groups
let group_configs = cells
let groups = cells
.into_iter()
.flat_map(|value| value.into_url_field_cell_data())
.filter(|cell| !cell.data.is_empty())
.map(|cell| GeneratedGroupConfig {
group: make_group_from_url_cell(&cell),
filter_content: cell.data,
})
.map(|cell| Group::new(cell.data.clone()))
.collect();
let no_status_group = Some(make_no_status_group(field));
GeneratedGroups {
no_status_group,
group_configs,
groups,
}
}
}
fn make_group_from_url_cell(cell: &URLCellData) -> Group {
let group_id = cell.data.clone();
let group_name = cell.data.clone();
Group::new(group_id, group_name)
}
pub struct URLGroupOperationInterceptorImpl {
#[allow(dead_code)]
pub(crate) cell_writer: Arc<dyn GroupTypeOptionCellOperation>,
}
#[async_trait::async_trait]
impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl {
type GroupTypeOption = URLTypeOption;
}

View File

@ -14,16 +14,6 @@ pub struct GroupSetting {
pub content: String,
}
pub struct GroupChangesets {
pub changesets: Vec<GroupChangeset>,
}
impl From<Vec<GroupChangeset>> for GroupChangesets {
fn from(changesets: Vec<GroupChangeset>) -> Self {
Self { changesets }
}
}
#[derive(Clone, Default, Debug)]
pub struct GroupChangeset {
pub group_id: String,
@ -92,7 +82,6 @@ impl From<GroupSetting> for GroupSettingMap {
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Group {
pub id: String,
pub name: String,
#[serde(default = "GROUP_VISIBILITY")]
pub visible: bool,
}
@ -104,9 +93,8 @@ impl TryFrom<GroupMap> for Group {
match value.get_str_value("id") {
None => bail!("Invalid group data"),
Some(id) => {
let name = value.get_str_value("name").unwrap_or_default();
let visible = value.get_bool_value("visible").unwrap_or_default();
Ok(Self { id, name, visible })
Ok(Self { id, visible })
},
}
}
@ -116,7 +104,6 @@ impl From<Group> for GroupMap {
fn from(group: Group) -> Self {
GroupMapBuilder::new()
.insert_str_value("id", group.id)
.insert_str_value("name", group.name)
.insert_bool_value("visible", group.visible)
.build()
}
@ -125,12 +112,8 @@ impl From<Group> for GroupMap {
const GROUP_VISIBILITY: fn() -> bool = || true;
impl Group {
pub fn new(id: String, name: String) -> Self {
Self {
id,
name,
visible: true,
}
pub fn new(id: String) -> Self {
Self { id, visible: true }
}
}
@ -138,32 +121,20 @@ impl Group {
pub struct GroupData {
pub id: String,
pub field_id: String,
pub name: String,
pub is_default: bool,
pub is_visible: bool,
pub(crate) rows: Vec<RowDetail>,
/// [filter_content] is used to determine which group the cell belongs to.
pub filter_content: String,
}
impl GroupData {
pub fn new(
id: String,
field_id: String,
name: String,
filter_content: String,
is_visible: bool,
) -> Self {
pub fn new(id: String, field_id: String, is_visible: bool) -> Self {
let is_default = id == field_id;
Self {
id,
field_id,
is_default,
is_visible,
name,
rows: vec![],
filter_content,
}
}

View File

@ -4,21 +4,17 @@ use std::sync::Arc;
use async_trait::async_trait;
use collab_database::fields::Field;
use collab_database::rows::{Cell, RowDetail, RowId};
use collab_database::views::DatabaseLayout;
use flowy_error::FlowyResult;
use crate::entities::FieldType;
use crate::services::field::TypeOption;
use crate::services::group::{
CheckboxGroupContext, CheckboxGroupController, CheckboxGroupOperationInterceptorImpl,
DateGroupContext, DateGroupController, DateGroupOperationInterceptorImpl, DefaultGroupController,
Group, GroupController, GroupSetting, GroupSettingReader, GroupSettingWriter,
GroupTypeOptionCellOperation, MultiSelectGroupController,
MultiSelectGroupOperationInterceptorImpl, MultiSelectOptionGroupContext,
SingleSelectGroupController, SingleSelectGroupOperationInterceptorImpl,
SingleSelectOptionGroupContext, URLGroupContext, URLGroupController,
URLGroupOperationInterceptorImpl,
CheckboxGroupController, CheckboxGroupControllerContext, DateGroupController,
DateGroupControllerContext, DefaultGroupController, Group, GroupContextDelegate, GroupController,
GroupControllerDelegate, GroupSetting, MultiSelectGroupController,
MultiSelectGroupControllerContext, SingleSelectGroupController,
SingleSelectGroupControllerContext, URLGroupController, URLGroupControllerContext,
};
/// The [GroupsBuilder] trait is used to generate the groups for different [FieldType]
@ -36,12 +32,7 @@ pub trait GroupsBuilder: Send + Sync + 'static {
pub struct GeneratedGroups {
pub no_status_group: Option<Group>,
pub group_configs: Vec<GeneratedGroupConfig>,
}
pub struct GeneratedGroupConfig {
pub group: Group,
pub filter_content: String,
pub groups: Vec<Group>,
}
pub struct MoveGroupRowContext<'a> {
@ -94,95 +85,56 @@ impl RowChangeset {
fields(grouping_field_id=%grouping_field.id, grouping_field_type)
err
)]
pub async fn make_group_controller<R, W, TW>(
pub async fn make_group_controller<D>(
view_id: String,
grouping_field: Field,
row_details: Vec<Arc<RowDetail>>,
setting_reader: R,
setting_writer: W,
type_option_cell_writer: TW,
delegate: D,
) -> FlowyResult<Box<dyn GroupController>>
where
R: GroupSettingReader,
W: GroupSettingWriter,
TW: GroupTypeOptionCellOperation,
D: GroupContextDelegate + GroupControllerDelegate,
{
let grouping_field_type = FieldType::from(grouping_field.field_type);
tracing::Span::current().record("grouping_field", &grouping_field_type.default_name());
let mut group_controller: Box<dyn GroupController>;
let configuration_reader = Arc::new(setting_reader);
let configuration_writer = Arc::new(setting_writer);
let type_option_cell_writer = Arc::new(type_option_cell_writer);
let delegate = Arc::new(delegate);
match grouping_field_type {
FieldType::SingleSelect => {
let configuration = SingleSelectOptionGroupContext::new(
view_id,
grouping_field.clone(),
configuration_reader,
configuration_writer,
)
let configuration =
SingleSelectGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone())
.await?;
let operation_interceptor = SingleSelectGroupOperationInterceptorImpl;
let controller =
SingleSelectGroupController::new(&grouping_field, configuration, operation_interceptor)
.await?;
SingleSelectGroupController::new(&grouping_field, configuration, delegate).await?;
group_controller = Box::new(controller);
},
FieldType::MultiSelect => {
let configuration = MultiSelectOptionGroupContext::new(
view_id,
grouping_field.clone(),
configuration_reader,
configuration_writer,
)
let configuration =
MultiSelectGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone())
.await?;
let operation_interceptor = MultiSelectGroupOperationInterceptorImpl;
let controller =
MultiSelectGroupController::new(&grouping_field, configuration, operation_interceptor)
.await?;
MultiSelectGroupController::new(&grouping_field, configuration, delegate).await?;
group_controller = Box::new(controller);
},
FieldType::Checkbox => {
let configuration = CheckboxGroupContext::new(
view_id,
grouping_field.clone(),
configuration_reader,
configuration_writer,
)
let configuration =
CheckboxGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone())
.await?;
let operation_interceptor = CheckboxGroupOperationInterceptorImpl {};
let controller =
CheckboxGroupController::new(&grouping_field, configuration, operation_interceptor).await?;
CheckboxGroupController::new(&grouping_field, configuration, delegate).await?;
group_controller = Box::new(controller);
},
FieldType::URL => {
let configuration = URLGroupContext::new(
view_id,
grouping_field.clone(),
configuration_reader,
configuration_writer,
)
.await?;
let operation_interceptor = URLGroupOperationInterceptorImpl {
cell_writer: type_option_cell_writer,
};
let controller =
URLGroupController::new(&grouping_field, configuration, operation_interceptor).await?;
let configuration =
URLGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()).await?;
let controller = URLGroupController::new(&grouping_field, configuration, delegate).await?;
group_controller = Box::new(controller);
},
FieldType::DateTime => {
let configuration = DateGroupContext::new(
view_id,
grouping_field.clone(),
configuration_reader,
configuration_writer,
)
.await?;
let operation_interceptor = DateGroupOperationInterceptorImpl {};
let controller =
DateGroupController::new(&grouping_field, configuration, operation_interceptor).await?;
let configuration =
DateGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()).await?;
let controller = DateGroupController::new(&grouping_field, configuration, delegate).await?;
group_controller = Box::new(controller);
},
_ => {
@ -194,32 +146,21 @@ where
let rows = row_details
.iter()
.map(|row| row.as_ref())
.collect::<Vec<&RowDetail>>();
.collect::<Vec<_>>();
group_controller.fill_groups(rows.as_slice(), &grouping_field)?;
Ok(group_controller)
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn find_new_grouping_field(fields: &[Field], _layout: &DatabaseLayout) -> Option<Field> {
let mut groupable_field_revs = fields
pub fn find_suitable_grouping_field(fields: &[Field]) -> Option<Field> {
let groupable_field = fields
.iter()
.flat_map(|field_rev| {
let field_type = FieldType::from(field_rev.field_type);
match field_type.can_be_group() {
true => Some(field_rev.clone()),
false => None,
}
})
.collect::<Vec<Field>>();
.find(|field| FieldType::from(field.field_type).can_be_group());
if groupable_field_revs.is_empty() {
// If there is not groupable fields then we use the primary field.
fields
.iter()
.find(|field_rev| field_rev.is_primary)
.cloned()
if let Some(field) = groupable_field {
Some(field.clone())
} else {
Some(groupable_field_revs.remove(0))
fields.iter().find(|field| field.is_primary).cloned()
}
}
@ -237,7 +178,6 @@ pub fn default_group_setting(field: &Field) -> GroupSetting {
pub fn make_no_status_group(field: &Field) -> Group {
Group {
id: field.id.clone(),
name: format!("No {}", field.name),
visible: true,
}
}

View File

@ -5,6 +5,7 @@ mod controller_impls;
mod entities;
mod group_builder;
pub(crate) use action::GroupController;
pub(crate) use configuration::*;
pub(crate) use controller::*;
pub(crate) use controller_impls::*;

View File

@ -63,56 +63,50 @@ async fn group_by_date_test() {
row_count: 0,
},
// Added via `make_test_board`
AssertGroupIDName {
AssertGroupId {
group_index: 1,
group_id: "2022/03/01".to_string(),
group_name: "Mar 2022".to_string(),
},
AssertGroupRowCount {
group_index: 1,
row_count: 3,
},
// Added via `make_test_board`
AssertGroupIDName {
AssertGroupId {
group_index: 2,
group_id: "2022/11/01".to_string(),
group_name: "Nov 2022".to_string(),
},
AssertGroupRowCount {
group_index: 2,
row_count: 2,
},
AssertGroupIDName {
AssertGroupId {
group_index: 3,
group_id: last_30_days,
group_name: "Last 30 days".to_string(),
},
AssertGroupRowCount {
group_index: 3,
row_count: 1,
},
AssertGroupIDName {
AssertGroupId {
group_index: 4,
group_id: last_day,
group_name: "Yesterday".to_string(),
},
AssertGroupRowCount {
group_index: 4,
row_count: 2,
},
AssertGroupIDName {
AssertGroupId {
group_index: 5,
group_id: today.format("%Y/%m/%d").to_string(),
group_name: "Today".to_string(),
},
AssertGroupRowCount {
group_index: 5,
row_count: 1,
},
AssertGroupIDName {
AssertGroupId {
group_index: 6,
group_id: next_7_days,
group_name: "Next 7 days".to_string(),
},
AssertGroupRowCount {
group_index: 6,
@ -180,10 +174,9 @@ async fn change_date_on_moving_row_to_another_group() {
group_index: 2,
row_count: 3,
},
AssertGroupIDName {
AssertGroupId {
group_index: 2,
group_id: "2022/11/01".to_string(),
group_name: "Nov 2022".to_string(),
},
];
test.run_scripts(scripts).await;

View File

@ -60,10 +60,9 @@ pub enum GroupScript {
GroupByField {
field_id: String,
},
AssertGroupIDName {
AssertGroupId {
group_index: usize,
group_id: String,
group_name: String,
},
CreateGroup {
name: String,
@ -241,7 +240,6 @@ impl DatabaseGroupTest {
} => {
let group = self.group_at_index(group_index).await;
assert_eq!(group.group_id, group_pb.group_id);
assert_eq!(group.group_name, group_pb.group_name);
},
GroupScript::UpdateSingleSelectSelectOption { inserted_options } => {
self
@ -259,14 +257,12 @@ impl DatabaseGroupTest {
.await
.unwrap();
},
GroupScript::AssertGroupIDName {
GroupScript::AssertGroupId {
group_index,
group_id,
group_name,
} => {
let group = self.group_at_index(group_index).await;
assert_eq!(group_id, group.group_id, "group index: {}", group_index);
assert_eq!(group_name, group.group_name, "group index: {}", group_index);
},
GroupScript::CreateGroup { name } => self
.editor

View File

@ -457,13 +457,17 @@ async fn group_insert_single_select_option_test() {
let scripts = vec![
AssertGroupCount(4),
UpdateSingleSelectSelectOption {
inserted_options: vec![SelectOption::new(new_option_name)],
inserted_options: vec![SelectOption {
id: new_option_name.to_string(),
name: new_option_name.to_string(),
color: Default::default(),
}],
},
AssertGroupCount(5),
];
test.run_scripts(scripts).await;
let new_group = test.group_at_index(4).await;
assert_eq!(new_group.group_name, new_option_name);
assert_eq!(new_group.group_id, new_option_name);
}
#[tokio::test]
@ -499,6 +503,4 @@ async fn group_manual_create_new_group() {
AssertGroupCount(5),
];
test.run_scripts(scripts).await;
let new_group = test.group_at_index(4).await;
assert_eq!(new_group.group_name, new_group_name);
}