feat: advanced filters backend logic (#4688)

* feat: implement advanced filters

* test: adapt tests to changes

* test: add advanced filter tests

* chore: adapt flutter frontend to changes

* chore: adapt tauri frontend to changes

* chore: bump collab

* chore: launch review

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Richard Shiue 2024-03-14 09:35:45 +08:00 committed by GitHub
parent 80e210b34a
commit 48cac4c5ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 1915 additions and 1514 deletions

View File

@ -162,75 +162,6 @@ class FieldController {
/// Listen for filter changes in the backend.
void _listenOnFilterChanges() {
void deleteFilterFromChangeset(
List<FilterInfo> filters,
FilterChangesetNotificationPB changeset,
) {
final deleteFilterIds = changeset.deleteFilters.map((e) => e.id).toList();
if (deleteFilterIds.isNotEmpty) {
filters.retainWhere(
(element) => !deleteFilterIds.contains(element.filter.id),
);
}
}
void insertFilterFromChangeset(
List<FilterInfo> filters,
FilterChangesetNotificationPB changeset,
) {
for (final newFilter in changeset.insertFilters) {
final filterIndex =
filters.indexWhere((element) => element.filter.id == newFilter.id);
if (filterIndex == -1) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: newFilter.fieldId,
fieldType: newFilter.fieldType,
);
if (fieldInfo != null) {
filters.add(FilterInfo(viewId, newFilter, fieldInfo));
}
}
}
}
void updateFilterFromChangeset(
List<FilterInfo> filters,
FilterChangesetNotificationPB changeset,
) {
for (final updatedFilter in changeset.updateFilters) {
final filterIndex = filters.indexWhere(
(element) => element.filter.id == updatedFilter.filterId,
);
// Remove the old filter
if (filterIndex != -1) {
filters.removeAt(filterIndex);
}
// Insert the filter if there is a filter and its field info is
// not null
if (updatedFilter.hasFilter()) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: updatedFilter.filter.fieldId,
fieldType: updatedFilter.filter.fieldType,
);
if (fieldInfo != null) {
// Insert the filter with the position: filterIndex, otherwise,
// append it to the end of the list.
final filterInfo =
FilterInfo(viewId, updatedFilter.filter, fieldInfo);
if (filterIndex != -1) {
filters.insert(filterIndex, filterInfo);
} else {
filters.add(filterInfo);
}
}
}
}
}
_filtersListener.start(
onFilterChanged: (result) {
if (_isDisposed) {
@ -239,15 +170,19 @@ class FieldController {
result.fold(
(FilterChangesetNotificationPB changeset) {
final List<FilterInfo> filters = filterInfos;
// delete removed filters
deleteFilterFromChangeset(filters, changeset);
final List<FilterInfo> filters = [];
for (final filter in changeset.filters.items) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: filter.data.fieldId,
fieldType: filter.data.fieldType,
);
// insert new filters
insertFilterFromChangeset(filters, changeset);
// edit modified filters
updateFilterFromChangeset(filters, changeset);
if (fieldInfo != null) {
final filterInfo = FilterInfo(viewId, filter, fieldInfo);
filters.add(filterInfo);
}
}
_filterNotifier?.filters = filters;
_updateFieldInfos();
@ -665,8 +600,8 @@ class FieldController {
FilterInfo? getFilterInfo(FilterPB filterPB) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
fieldId: filterPB.fieldId,
fieldType: filterPB.fieldType,
fieldId: filterPB.data.fieldId,
fieldType: filterPB.data.fieldType,
);
return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null;
}

View File

@ -61,19 +61,11 @@ class FilterListener {
final String viewId;
final String filterId;
PublishNotifier<FilterPB>? _onDeleteNotifier = PublishNotifier();
PublishNotifier<FilterPB>? _onUpdateNotifier = PublishNotifier();
DatabaseNotificationListener? _listener;
void start({
void Function()? onDeleted,
void Function(FilterPB)? onUpdated,
}) {
_onDeleteNotifier?.addPublishListener((_) {
onDeleted?.call();
});
void start({void Function(FilterPB)? onUpdated}) {
_onUpdateNotifier?.addPublishListener((filter) {
onUpdated?.call(filter);
});
@ -85,20 +77,12 @@ class FilterListener {
}
void handleChangeset(FilterChangesetNotificationPB changeset) {
// check the delete filter
final deletedIndex = changeset.deleteFilters.indexWhere(
(element) => element.id == filterId,
);
if (deletedIndex != -1) {
_onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex];
}
// check the updated filter
final updatedIndex = changeset.updateFilters.indexWhere(
(element) => element.filter.id == filterId,
final filters = changeset.filters.items;
final updatedIndex = filters.indexWhere(
(filter) => filter.id == filterId,
);
if (updatedIndex != -1) {
_onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter;
_onUpdateNotifier?.value = filters[updatedIndex];
}
}
@ -122,9 +106,6 @@ class FilterListener {
Future<void> stop() async {
await _listener?.stop();
_onDeleteNotifier?.dispose();
_onDeleteNotifier = null;
_onUpdateNotifier?.dispose();
_onUpdateNotifier = null;
}

View File

@ -40,12 +40,18 @@ class FilterBackendService {
..condition = condition
..content = content;
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.RichText,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.RichText,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.RichText,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertCheckboxFilter({
@ -55,12 +61,18 @@ class FilterBackendService {
}) {
final filter = CheckboxFilterPB()..condition = condition;
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.Checkbox,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.Checkbox,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.Checkbox,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertNumberFilter({
@ -73,12 +85,18 @@ class FilterBackendService {
..condition = condition
..content = content;
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.Number,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.Number,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.Number,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertDateFilter({
@ -112,12 +130,18 @@ class FilterBackendService {
}
}
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: fieldType,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.DateTime,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.DateTime,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertURLFilter({
@ -130,12 +154,18 @@ class FilterBackendService {
..condition = condition
..content = content;
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.URL,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.URL,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.URL,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertSelectOptionFilter({
@ -149,12 +179,18 @@ class FilterBackendService {
..condition = condition
..optionIds.addAll(optionIds);
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: fieldType,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: fieldType,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: fieldType,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertChecklistFilter({
@ -165,67 +201,94 @@ class FilterBackendService {
}) {
final filter = ChecklistFilterPB()..condition = condition;
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.Checklist,
data: filter.writeToBuffer(),
);
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.Checklist,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.Checklist,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertFilter({
required String fieldId,
String? filterId,
required FieldType fieldType,
required List<int> data,
}) {
final insertFilterPayload = UpdateFilterPayloadPB.create()
}) async {
final filterData = FilterDataPB()
..fieldId = fieldId
..fieldType = fieldType
..viewId = viewId
..data = data;
if (filterId != null) {
insertFilterPayload.filterId = filterId;
}
final insertFilterPayload = InsertFilterPB()..data = filterData;
final payload = DatabaseSettingChangesetPB.create()
final payload = DatabaseSettingChangesetPB()
..viewId = viewId
..updateFilter = insertFilterPayload;
return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {
return result.fold(
(l) => FlowyResult.success(l),
(err) {
Log.error(err);
return FlowyResult.failure(err);
},
);
});
..insertFilter = insertFilterPayload;
final result = await DatabaseEventUpdateDatabaseSetting(payload).send();
return result.fold(
(l) => FlowyResult.success(l),
(err) {
Log.error(err);
return FlowyResult.failure(err);
},
);
}
Future<FlowyResult<void, FlowyError>> updateFilter({
required String filterId,
required String fieldId,
required FieldType fieldType,
required List<int> data,
}) async {
final filterData = FilterDataPB()
..fieldId = fieldId
..fieldType = fieldType
..data = data;
final updateFilterPayload = UpdateFilterDataPB()
..filterId = filterId
..data = filterData;
final payload = DatabaseSettingChangesetPB()
..viewId = viewId
..updateFilterData = updateFilterPayload;
final result = await DatabaseEventUpdateDatabaseSetting(payload).send();
return result.fold(
(l) => FlowyResult.success(l),
(err) {
Log.error(err);
return FlowyResult.failure(err);
},
);
}
Future<FlowyResult<void, FlowyError>> deleteFilter({
required String fieldId,
required String filterId,
required FieldType fieldType,
}) {
final deleteFilterPayload = DeleteFilterPayloadPB.create()
}) async {
final deleteFilterPayload = DeleteFilterPB()
..fieldId = fieldId
..filterId = filterId
..viewId = viewId
..fieldType = fieldType;
..filterId = filterId;
final payload = DatabaseSettingChangesetPB.create()
final payload = DatabaseSettingChangesetPB()
..viewId = viewId
..deleteFilter = deleteFilterPayload;
return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {
return result.fold(
(l) => FlowyResult.success(l),
(err) {
Log.error(err);
return FlowyResult.failure(err);
},
);
});
final result = await DatabaseEventUpdateDatabaseSetting(payload).send();
return result.fold(
(l) => FlowyResult.success(l),
(err) {
Log.error(err);
return FlowyResult.failure(err);
},
);
}
}

View File

@ -44,7 +44,6 @@ class CheckboxFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@ -64,11 +63,10 @@ class CheckboxFilterEditorBloc
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const CheckboxFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
if (!isClosed) {
add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
}
},
);
}

View File

@ -44,7 +44,6 @@ class ChecklistFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@ -64,9 +63,6 @@ class ChecklistFilterEditorBloc
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const ChecklistFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) {
add(ChecklistFilterEditorEvent.didReceiveFilter(filter));

View File

@ -59,7 +59,6 @@ class NumberFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.fieldInfo.fieldType,
);
},
);
@ -69,11 +68,6 @@ class NumberFilterEditorBloc
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) {
add(const NumberFilterEditorEvent.delete());
}
},
onUpdated: (filter) {
if (!isClosed) {
add(NumberFilterEditorEvent.didReceiveFilter(filter));

View File

@ -60,7 +60,6 @@ class SelectOptionFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@ -83,9 +82,6 @@ class SelectOptionFilterEditorBloc
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const SelectOptionFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) {
add(SelectOptionFilterEditorEvent.didReceiveFilter(filter));

View File

@ -53,7 +53,6 @@ class TextFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@ -73,11 +72,10 @@ class TextFilterEditorBloc
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const TextFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter));
if (!isClosed) {
add(TextFilterEditorEvent.didReceiveFilter(filter));
}
},
);
}

View File

@ -18,42 +18,42 @@ class FilterInfo {
String get filterId => filter.id;
String get fieldId => filter.fieldId;
String get fieldId => filter.data.fieldId;
DateFilterPB? dateFilter() {
return filter.fieldType == FieldType.DateTime
? DateFilterPB.fromBuffer(filter.data)
return filter.data.fieldType == FieldType.DateTime
? DateFilterPB.fromBuffer(filter.data.data)
: null;
}
TextFilterPB? textFilter() {
return filter.fieldType == FieldType.RichText
? TextFilterPB.fromBuffer(filter.data)
return filter.data.fieldType == FieldType.RichText
? TextFilterPB.fromBuffer(filter.data.data)
: null;
}
CheckboxFilterPB? checkboxFilter() {
return filter.fieldType == FieldType.Checkbox
? CheckboxFilterPB.fromBuffer(filter.data)
return filter.data.fieldType == FieldType.Checkbox
? CheckboxFilterPB.fromBuffer(filter.data.data)
: null;
}
SelectOptionFilterPB? selectOptionFilter() {
return filter.fieldType == FieldType.SingleSelect ||
filter.fieldType == FieldType.MultiSelect
? SelectOptionFilterPB.fromBuffer(filter.data)
return filter.data.fieldType == FieldType.SingleSelect ||
filter.data.fieldType == FieldType.MultiSelect
? SelectOptionFilterPB.fromBuffer(filter.data.data)
: null;
}
ChecklistFilterPB? checklistFilter() {
return filter.fieldType == FieldType.Checklist
? ChecklistFilterPB.fromBuffer(filter.data)
return filter.data.fieldType == FieldType.Checklist
? ChecklistFilterPB.fromBuffer(filter.data.data)
: null;
}
NumberFilterPB? numberFilter() {
return filter.fieldType == FieldType.Number
? NumberFilterPB.fromBuffer(filter.data)
return filter.data.fieldType == FieldType.Number
? NumberFilterPB.fromBuffer(filter.data.data)
: null;
}
}

View File

@ -42,7 +42,6 @@ void main() {
await service.deleteFilter(
fieldId: textField.id,
filterId: filterInfo.filter.id,
fieldType: textField.fieldType,
);
await gridResponseFuture();

View File

@ -37,7 +37,6 @@ void main() {
await service.deleteFilter(
fieldId: textField.id,
filterId: textFilter.filter.id,
fieldType: textField.fieldType,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
@ -65,7 +64,6 @@ void main() {
await service.deleteFilter(
fieldId: textField.id,
filterId: textFilter.filter.id,
fieldType: textField.fieldType,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
@ -107,7 +105,6 @@ void main() {
await service.deleteFilter(
fieldId: textField.id,
filterId: textFilter.filter.id,
fieldType: textField.fieldType,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);
@ -165,7 +162,6 @@ void main() {
await service.deleteFilter(
fieldId: textField.id,
filterId: textFilter.filter.id,
fieldType: textField.fieldType,
);
await gridResponseFuture();
assert(context.rowInfos.length == 3);

View File

@ -34,7 +34,8 @@ lru = "0.12.0"
[dependencies]
serde_json.workspace = true
serde.workspace = true
tauri = { version = "1.5", features = [ "dialog-all",
tauri = { version = "1.5", features = [
"dialog-all",
"clipboard-all",
"fs-all",
"shell-open",

View File

@ -1,35 +1,8 @@
import { Database, pbToFilter } from '$app/application/database';
import { FilterChangesetNotificationPB } from '@/services/backend';
const deleteFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => {
const deleteIds = changeset.delete_filters.map((pb) => pb.id);
if (deleteIds.length) {
database.filters = database.filters.filter((item) => !deleteIds.includes(item.id));
}
};
const insertFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => {
changeset.insert_filters.forEach((pb) => {
database.filters.push(pbToFilter(pb));
});
};
const updateFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => {
changeset.update_filters.forEach((pb) => {
const found = database.filters.find((item) => item.id === pb.filter_id);
if (found) {
const newFilter = pbToFilter(pb.filter);
Object.assign(found, newFilter);
database.filters = [...database.filters];
}
});
};
export const didUpdateFilter = (database: Database, changeset: FilterChangesetNotificationPB) => {
deleteFiltersFromChange(database, changeset);
insertFiltersFromChange(database, changeset);
updateFiltersFromChange(database, changeset);
const filters = changeset.filters.items.map((pb) => pbToFilter(pb));
database.filters = filters;
};

View File

@ -21,22 +21,20 @@ export async function insertFilter({
fieldId,
fieldType,
data,
filterId,
}: {
viewId: string;
fieldId: string;
fieldType: FieldType;
data?: UndeterminedFilter['data'];
filterId?: string;
}): Promise<void> {
const payload = DatabaseSettingChangesetPB.fromObject({
view_id: viewId,
update_filter: {
view_id: viewId,
field_id: fieldId,
field_type: fieldType,
filter_id: filterId,
data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined,
insert_filter: {
data: {
field_id: fieldId,
field_type: fieldType,
data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined,
},
},
});
@ -52,12 +50,13 @@ export async function insertFilter({
export async function updateFilter(viewId: string, filter: UndeterminedFilter): Promise<void> {
const payload = DatabaseSettingChangesetPB.fromObject({
view_id: viewId,
update_filter: {
view_id: viewId,
update_filter_data: {
filter_id: filter.id,
field_id: filter.fieldId,
field_type: filter.fieldType,
data: filterDataToPB(filter.data, filter.fieldType)?.serialize(),
data: {
field_id: filter.fieldId,
field_type: filter.fieldType,
data: filterDataToPB(filter.data, filter.fieldType)?.serialize(),
},
},
});
@ -74,10 +73,8 @@ export async function deleteFilter(viewId: string, filter: Omit<Filter, 'data'>)
const payload = DatabaseSettingChangesetPB.fromObject({
view_id: viewId,
delete_filter: {
view_id: viewId,
filter_id: filter.id,
field_id: filter.fieldId,
field_type: filter.fieldType,
},
});

View File

@ -195,8 +195,8 @@ export function bytesToFilterData(bytes: Uint8Array, fieldType: FieldType) {
export function pbToFilter(pb: FilterPB): Filter {
return {
id: pb.id,
fieldId: pb.field_id,
fieldType: pb.field_type,
data: bytesToFilterData(pb.data, pb.field_type),
fieldId: pb.data.field_id,
fieldType: pb.data.field_type,
data: bytesToFilterData(pb.data.data, pb.data.field_type),
};
}

View File

@ -1,7 +1,7 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct CheckboxFilterPB {
@ -9,9 +9,8 @@ pub struct CheckboxFilterPB {
pub condition: CheckboxFilterConditionPB,
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
#[derive(Default)]
pub enum CheckboxFilterConditionPB {
#[default]
IsChecked = 0,
@ -24,7 +23,7 @@ impl std::convert::From<CheckboxFilterConditionPB> for u32 {
}
}
impl std::convert::TryFrom<u8> for CheckboxFilterConditionPB {
impl TryFrom<u8> for CheckboxFilterConditionPB {
type Error = ErrorCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
@ -36,22 +35,10 @@ impl std::convert::TryFrom<u8> for CheckboxFilterConditionPB {
}
}
impl FromFilterString for CheckboxFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
impl ParseFilterData for CheckboxFilterPB {
fn parse(condition: u8, _content: String) -> Self {
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(CheckboxFilterConditionPB::IsChecked),
}
}
}
impl std::convert::From<&Filter> for CheckboxFilterPB {
fn from(filter: &Filter) -> Self {
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::try_from(filter.condition as u8)
condition: CheckboxFilterConditionPB::try_from(condition)
.unwrap_or(CheckboxFilterConditionPB::IsChecked),
}
}

View File

@ -1,7 +1,7 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct ChecklistFilterPB {
@ -36,22 +36,10 @@ impl std::convert::TryFrom<u8> for ChecklistFilterConditionPB {
}
}
impl FromFilterString for ChecklistFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
ChecklistFilterPB {
condition: ChecklistFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(ChecklistFilterConditionPB::IsIncomplete),
}
}
}
impl std::convert::From<&Filter> for ChecklistFilterPB {
fn from(filter: &Filter) -> Self {
ChecklistFilterPB {
condition: ChecklistFilterConditionPB::try_from(filter.condition as u8)
impl ParseFilterData for ChecklistFilterPB {
fn parse(condition: u8, _content: String) -> Self {
Self {
condition: ChecklistFilterConditionPB::try_from(condition)
.unwrap_or(ChecklistFilterConditionPB::IsIncomplete),
}
}

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct DateFilterPB {
@ -79,37 +79,17 @@ impl std::convert::TryFrom<u8> for DateFilterConditionPB {
}
}
}
impl FromFilterString for DateFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
let condition = DateFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(DateFilterConditionPB::DateIs);
let mut date_filter = DateFilterPB {
impl ParseFilterData for DateFilterPB {
fn parse(condition: u8, content: String) -> Self {
let condition =
DateFilterConditionPB::try_from(condition).unwrap_or(DateFilterConditionPB::DateIs);
let mut date_filter = Self {
condition,
..Default::default()
};
if let Ok(content) = DateFilterContentPB::from_str(&filter.content) {
date_filter.start = content.start;
date_filter.end = content.end;
date_filter.timestamp = content.timestamp;
};
date_filter
}
}
impl std::convert::From<&Filter> for DateFilterPB {
fn from(filter: &Filter) -> Self {
let condition = DateFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(DateFilterConditionPB::DateIs);
let mut date_filter = DateFilterPB {
condition,
..Default::default()
};
if let Ok(content) = DateFilterContentPB::from_str(&filter.content) {
if let Ok(content) = DateFilterContentPB::from_str(&content) {
date_filter.start = content.start;
date_filter.end = content.end;
date_filter.timestamp = content.timestamp;

View File

@ -1,54 +1,22 @@
use crate::entities::FilterPB;
use flowy_derive::ProtoBuf;
use crate::entities::RepeatedFilterPB;
use crate::services::filter::Filter;
#[derive(Debug, Default, ProtoBuf)]
pub struct FilterChangesetNotificationPB {
#[pb(index = 1)]
pub view_id: String,
#[pb(index = 2)]
pub insert_filters: Vec<FilterPB>,
#[pb(index = 3)]
pub delete_filters: Vec<FilterPB>,
#[pb(index = 4)]
pub update_filters: Vec<UpdatedFilter>,
}
#[derive(Debug, Default, ProtoBuf)]
pub struct UpdatedFilter {
#[pb(index = 1)]
pub filter_id: String,
#[pb(index = 2, one_of)]
pub filter: Option<FilterPB>,
pub filters: RepeatedFilterPB,
}
impl FilterChangesetNotificationPB {
pub fn from_insert(view_id: &str, filters: Vec<FilterPB>) -> Self {
pub fn from_filters(view_id: &str, filters: &Vec<Filter>) -> Self {
Self {
view_id: view_id.to_string(),
insert_filters: filters,
delete_filters: Default::default(),
update_filters: Default::default(),
}
}
pub fn from_delete(view_id: &str, filters: Vec<FilterPB>) -> Self {
Self {
view_id: view_id.to_string(),
insert_filters: Default::default(),
delete_filters: filters,
update_filters: Default::default(),
}
}
pub fn from_update(view_id: &str, filters: Vec<UpdatedFilter>) -> Self {
Self {
view_id: view_id.to_string(),
insert_filters: Default::default(),
delete_filters: Default::default(),
update_filters: filters,
filters: filters.into(),
}
}
}

View File

@ -1,7 +1,7 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct NumberFilterPB {
@ -49,24 +49,12 @@ impl std::convert::TryFrom<u8> for NumberFilterConditionPB {
}
}
impl FromFilterString for NumberFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
impl ParseFilterData for NumberFilterPB {
fn parse(condition: u8, content: String) -> Self {
NumberFilterPB {
condition: NumberFilterConditionPB::try_from(filter.condition as u8)
condition: NumberFilterConditionPB::try_from(condition)
.unwrap_or(NumberFilterConditionPB::Equal),
content: filter.content.clone(),
}
}
}
impl std::convert::From<&Filter> for NumberFilterPB {
fn from(filter: &Filter) -> Self {
NumberFilterPB {
condition: NumberFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(NumberFilterConditionPB::Equal),
content: filter.content.clone(),
content,
}
}
}

View File

@ -1,6 +1,6 @@
use flowy_derive::ProtoBuf;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RelationFilterPB {
@ -8,17 +8,8 @@ pub struct RelationFilterPB {
pub condition: i64,
}
impl FromFilterString for RelationFilterPB {
fn from_filter(_filter: &Filter) -> Self
where
Self: Sized,
{
RelationFilterPB { condition: 0 }
}
}
impl From<&Filter> for RelationFilterPB {
fn from(_filter: &Filter) -> Self {
impl ParseFilterData for RelationFilterPB {
fn parse(_condition: u8, _content: String) -> Self {
RelationFilterPB { condition: 0 }
}
}

View File

@ -3,8 +3,7 @@ use std::str::FromStr;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::services::field::SelectOptionIds;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::{field::SelectOptionIds, filter::ParseFilterData};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct SelectOptionFilterPB {
@ -45,27 +44,14 @@ impl std::convert::TryFrom<u8> for SelectOptionConditionPB {
}
}
}
impl FromFilterString for SelectOptionFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
let ids = SelectOptionIds::from_str(&filter.content).unwrap_or_default();
SelectOptionFilterPB {
condition: SelectOptionConditionPB::try_from(filter.condition as u8)
impl ParseFilterData for SelectOptionFilterPB {
fn parse(condition: u8, content: String) -> Self {
Self {
condition: SelectOptionConditionPB::try_from(condition)
.unwrap_or(SelectOptionConditionPB::OptionIs),
option_ids: ids.into_inner(),
}
}
}
impl std::convert::From<&Filter> for SelectOptionFilterPB {
fn from(filter: &Filter) -> Self {
let ids = SelectOptionIds::from_str(&filter.content).unwrap_or_default();
SelectOptionFilterPB {
condition: SelectOptionConditionPB::try_from(filter.condition as u8)
.unwrap_or(SelectOptionConditionPB::OptionIs),
option_ids: ids.into_inner(),
option_ids: SelectOptionIds::from_str(&content)
.unwrap_or_default()
.into_inner(),
}
}
}

View File

@ -1,7 +1,7 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use crate::services::filter::{Filter, FromFilterString};
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct TextFilterPB {
@ -51,25 +51,11 @@ impl std::convert::TryFrom<u8> for TextFilterConditionPB {
}
}
impl FromFilterString for TextFilterPB {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized,
{
TextFilterPB {
condition: TextFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(TextFilterConditionPB::Is),
content: filter.content.clone(),
}
}
}
impl std::convert::From<&Filter> for TextFilterPB {
fn from(filter: &Filter) -> Self {
TextFilterPB {
condition: TextFilterConditionPB::try_from(filter.condition as u8)
.unwrap_or(TextFilterConditionPB::Is),
content: filter.content.clone(),
impl ParseFilterData for TextFilterPB {
fn parse(condition: u8, content: String) -> Self {
Self {
condition: TextFilterConditionPB::try_from(condition).unwrap_or(TextFilterConditionPB::Is),
content,
}
}
}

View File

@ -1,216 +1,274 @@
use std::convert::TryInto;
use std::sync::Arc;
use bytes::Bytes;
use collab_database::fields::Field;
use flowy_derive::ProtoBuf;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use lib_infra::box_any::BoxAny;
use protobuf::ProtobufError;
use validator::Validate;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType,
NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB,
SelectOptionFilterPB, TextFilterPB,
};
use crate::services::field::SelectOptionIds;
use crate::services::filter::Filter;
use crate::services::filter::{Filter, FilterChangeset, FilterInner};
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
#[derive(Debug, Default, Clone, ProtoBuf_Enum, Eq, PartialEq, Copy)]
#[repr(u8)]
pub enum FilterType {
#[default]
Data = 0,
And = 1,
Or = 2,
}
impl From<&FilterInner> for FilterType {
fn from(value: &FilterInner) -> Self {
match value {
FilterInner::And { .. } => Self::And,
FilterInner::Or { .. } => Self::Or,
FilterInner::Data { .. } => Self::Data,
}
}
}
#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)]
pub struct FilterPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub field_id: String,
pub filter_type: FilterType,
#[pb(index = 3)]
pub children: Vec<FilterPB>,
#[pb(index = 4, one_of)]
pub data: Option<FilterDataPB>,
}
#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)]
pub struct FilterDataPB {
#[pb(index = 1)]
pub field_id: String,
#[pb(index = 2)]
pub field_type: FieldType,
#[pb(index = 4)]
#[pb(index = 3)]
pub data: Vec<u8>,
}
impl std::convert::From<&Filter> for FilterPB {
impl From<&Filter> for FilterPB {
fn from(filter: &Filter) -> Self {
let bytes: Bytes = match filter.field_type {
FieldType::RichText => TextFilterPB::from(filter).try_into().unwrap(),
FieldType::Number => NumberFilterPB::from(filter).try_into().unwrap(),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
DateFilterPB::from(filter).try_into().unwrap()
match &filter.inner {
FilterInner::And { children } | FilterInner::Or { children } => Self {
id: filter.id.clone(),
filter_type: FilterType::from(&filter.inner),
children: children.iter().map(FilterPB::from).collect(),
data: None,
},
FilterInner::Data {
field_id,
field_type,
condition_and_content,
} => {
let bytes: Result<Bytes, ProtobufError> = match field_type {
FieldType::RichText | FieldType::URL => condition_and_content
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
FieldType::Number => condition_and_content
.cloned::<NumberFilterPB>()
.unwrap()
.try_into(),
FieldType::DateTime | FieldType::CreatedTime | FieldType::LastEditedTime => {
condition_and_content
.cloned::<DateFilterPB>()
.unwrap()
.try_into()
},
FieldType::SingleSelect | FieldType::MultiSelect => condition_and_content
.cloned::<SelectOptionFilterPB>()
.unwrap()
.try_into(),
FieldType::Checklist => condition_and_content
.cloned::<ChecklistFilterPB>()
.unwrap()
.try_into(),
FieldType::Checkbox => condition_and_content
.cloned::<CheckboxFilterPB>()
.unwrap()
.try_into(),
FieldType::Relation => condition_and_content
.cloned::<RelationFilterPB>()
.unwrap()
.try_into(),
};
Self {
id: filter.id.clone(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: field_id.clone(),
field_type: *field_type,
data: bytes.unwrap().to_vec(),
}),
}
},
FieldType::SingleSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(),
FieldType::MultiSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(),
FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(),
FieldType::Checkbox => CheckboxFilterPB::from(filter).try_into().unwrap(),
FieldType::URL => TextFilterPB::from(filter).try_into().unwrap(),
FieldType::Relation => RelationFilterPB::from(filter).try_into().unwrap(),
};
Self {
id: filter.id.clone(),
field_id: filter.field_id.clone(),
field_type: filter.field_type,
data: bytes.to_vec(),
}
}
}
impl TryFrom<FilterDataPB> for FilterInner {
type Error = ErrorCode;
fn try_from(value: FilterDataPB) -> Result<Self, Self::Error> {
let bytes: &[u8] = value.data.as_ref();
let condition_and_content = match value.field_type {
FieldType::RichText | FieldType::URL => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Checkbox => {
BoxAny::new(CheckboxFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Number => {
BoxAny::new(NumberFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
BoxAny::new(DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::SingleSelect | FieldType::MultiSelect => {
BoxAny::new(SelectOptionFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Checklist => {
BoxAny::new(ChecklistFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Relation => {
BoxAny::new(RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
};
Ok(Self::Data {
field_id: value.field_id,
field_type: value.field_type,
condition_and_content,
})
}
}
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct RepeatedFilterPB {
#[pb(index = 1)]
pub items: Vec<FilterPB>,
}
impl std::convert::From<Vec<Arc<Filter>>> for RepeatedFilterPB {
fn from(filters: Vec<Arc<Filter>>) -> Self {
impl From<&Vec<Filter>> for RepeatedFilterPB {
fn from(filters: &Vec<Filter>) -> Self {
RepeatedFilterPB {
items: filters.into_iter().map(|rev| rev.as_ref().into()).collect(),
items: filters.iter().map(|filter| filter.into()).collect(),
}
}
}
impl std::convert::From<Vec<FilterPB>> for RepeatedFilterPB {
impl From<Vec<FilterPB>> for RepeatedFilterPB {
fn from(items: Vec<FilterPB>) -> Self {
Self { items }
}
}
#[derive(ProtoBuf, Debug, Default, Clone, Validate)]
pub struct DeleteFilterPayloadPB {
#[pb(index = 1)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")]
pub field_id: String,
pub struct InsertFilterPB {
/// If None, the filter will be the root of a new filter tree
#[pb(index = 1, one_of)]
#[validate(custom = "crate::entities::utils::validate_filter_id")]
pub parent_filter_id: Option<String>,
#[pb(index = 2)]
pub field_type: FieldType,
#[pb(index = 3)]
#[validate(custom = "crate::entities::utils::validate_filter_id")]
pub filter_id: String,
#[pb(index = 4)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")]
pub view_id: String,
pub data: FilterDataPB,
}
#[derive(ProtoBuf, Debug, Default, Clone, Validate)]
pub struct UpdateFilterPayloadPB {
pub struct UpdateFilterTypePB {
#[pb(index = 1)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")]
pub field_id: String,
#[validate(custom = "crate::entities::utils::validate_filter_id")]
pub filter_id: String,
#[pb(index = 2)]
pub field_type: FieldType,
pub filter_type: FilterType,
}
/// Create a new filter if the filter_id is None
#[pb(index = 3, one_of)]
#[derive(ProtoBuf, Debug, Default, Clone, Validate)]
pub struct UpdateFilterDataPB {
#[pb(index = 1)]
#[validate(custom = "crate::entities::utils::validate_filter_id")]
pub filter_id: Option<String>,
pub filter_id: String,
#[pb(index = 4)]
pub data: Vec<u8>,
#[pb(index = 2)]
pub data: FilterDataPB,
}
#[pb(index = 5)]
#[derive(ProtoBuf, Debug, Default, Clone, Validate)]
pub struct DeleteFilterPB {
#[pb(index = 1)]
#[validate(custom = "crate::entities::utils::validate_filter_id")]
pub filter_id: String,
#[pb(index = 2)]
#[validate(custom = "lib_infra::validator_fn::required_not_empty_str")]
pub view_id: String,
pub field_id: String,
}
impl UpdateFilterPayloadPB {
#[allow(dead_code)]
pub fn new<T: TryInto<Bytes, Error = ::protobuf::ProtobufError>>(
view_id: &str,
field: &Field,
data: T,
) -> Self {
let data = data.try_into().unwrap_or_else(|_| Bytes::new());
let field_type = FieldType::from(field.field_type);
Self {
view_id: view_id.to_owned(),
field_id: field.id.clone(),
field_type,
filter_id: None,
data: data.to_vec(),
}
}
}
impl TryInto<UpdateFilterParams> for UpdateFilterPayloadPB {
impl TryFrom<InsertFilterPB> for FilterChangeset {
type Error = ErrorCode;
fn try_into(self) -> Result<UpdateFilterParams, Self::Error> {
let view_id = NotEmptyStr::parse(self.view_id)
.map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?
.0;
let field_id = NotEmptyStr::parse(self.field_id)
.map_err(|_| ErrorCode::FieldIdIsEmpty)?
.0;
let filter_id = match self.filter_id {
None => None,
Some(filter_id) => Some(
NotEmptyStr::parse(filter_id)
.map_err(|_| ErrorCode::FilterIdIsEmpty)?
.0,
),
fn try_from(value: InsertFilterPB) -> Result<Self, Self::Error> {
let changeset = Self::Insert {
parent_filter_id: value.parent_filter_id,
data: value.data.try_into()?,
};
let condition;
let mut content = "".to_string();
let bytes: &[u8] = self.data.as_ref();
match self.field_type {
FieldType::RichText | FieldType::URL => {
let filter = TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = filter.content;
},
FieldType::Checkbox => {
let filter = CheckboxFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
},
FieldType::Number => {
let filter = NumberFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = filter.content;
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
let filter = DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = DateFilterContentPB {
start: filter.start,
end: filter.end,
timestamp: filter.timestamp,
}
.to_string();
},
FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => {
let filter = SelectOptionFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
content = SelectOptionIds::from(filter.option_ids).to_string();
},
FieldType::Relation => {
let filter = RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
condition = filter.condition as u8;
},
}
Ok(UpdateFilterParams {
view_id,
field_id,
filter_id,
field_type: self.field_type,
condition: condition as i64,
content,
})
Ok(changeset)
}
}
#[derive(Debug)]
pub struct UpdateFilterParams {
pub view_id: String,
pub field_id: String,
/// Create a new filter if the filter_id is None
pub filter_id: Option<String>,
pub field_type: FieldType,
pub condition: i64,
pub content: String,
impl TryFrom<UpdateFilterDataPB> for FilterChangeset {
type Error = ErrorCode;
fn try_from(value: UpdateFilterDataPB) -> Result<Self, Self::Error> {
let changeset = Self::UpdateData {
filter_id: value.filter_id,
data: value.data.try_into()?,
};
Ok(changeset)
}
}
impl TryFrom<UpdateFilterTypePB> for FilterChangeset {
type Error = ErrorCode;
fn try_from(value: UpdateFilterTypePB) -> Result<Self, Self::Error> {
if matches!(value.filter_type, FilterType::Data) {
return Err(ErrorCode::InvalidParams);
}
let changeset = Self::UpdateType {
filter_id: value.filter_id,
filter_type: value.filter_type,
};
Ok(changeset)
}
}
impl From<DeleteFilterPB> for FilterChangeset {
fn from(value: DeleteFilterPB) -> Self {
Self::Delete {
filter_id: value.filter_id,
field_id: value.field_id,
}
}
}

View File

@ -9,9 +9,9 @@ use validator::Validate;
use crate::entities::parser::NotEmptyStr;
use crate::entities::{
CalendarLayoutSettingPB, DeleteFilterPayloadPB, DeleteSortPayloadPB, RepeatedFieldSettingsPB,
RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB, UpdateFilterPayloadPB, UpdateGroupPB,
UpdateSortPayloadPB,
CalendarLayoutSettingPB, DeleteFilterPB, DeleteSortPayloadPB, InsertFilterPB,
RepeatedFieldSettingsPB, RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB,
UpdateFilterDataPB, UpdateFilterTypePB, UpdateGroupPB, UpdateSortPayloadPB,
};
use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting};
@ -79,26 +79,34 @@ pub struct DatabaseSettingChangesetPB {
#[pb(index = 3, one_of)]
#[validate]
pub update_filter: Option<UpdateFilterPayloadPB>,
pub insert_filter: Option<InsertFilterPB>,
#[pb(index = 4, one_of)]
#[validate]
pub delete_filter: Option<DeleteFilterPayloadPB>,
pub update_filter_type: Option<UpdateFilterTypePB>,
#[pb(index = 5, one_of)]
#[validate]
pub update_group: Option<UpdateGroupPB>,
pub update_filter_data: Option<UpdateFilterDataPB>,
#[pb(index = 6, one_of)]
#[validate]
pub update_sort: Option<UpdateSortPayloadPB>,
pub delete_filter: Option<DeleteFilterPB>,
#[pb(index = 7, one_of)]
#[validate]
pub reorder_sort: Option<ReorderSortPayloadPB>,
pub update_group: Option<UpdateGroupPB>,
#[pb(index = 8, one_of)]
#[validate]
pub update_sort: Option<UpdateSortPayloadPB>,
#[pb(index = 9, one_of)]
#[validate]
pub reorder_sort: Option<ReorderSortPayloadPB>,
#[pb(index = 10, one_of)]
#[validate]
pub delete_sort: Option<DeleteSortPayloadPB>,
}

View File

@ -91,14 +91,28 @@ pub(crate) async fn update_database_setting_handler(
let params = data.try_into_inner()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
if let Some(update_filter) = params.update_filter {
if let Some(payload) = params.insert_filter {
database_editor
.create_or_update_filter(update_filter.try_into()?)
.modify_view_filters(&params.view_id, payload.try_into()?)
.await?;
}
if let Some(delete_filter) = params.delete_filter {
database_editor.delete_filter(delete_filter).await?;
if let Some(payload) = params.update_filter_type {
database_editor
.modify_view_filters(&params.view_id, payload.try_into()?)
.await?;
}
if let Some(payload) = params.update_filter_data {
database_editor
.modify_view_filters(&params.view_id, payload.try_into()?)
.await?;
}
if let Some(payload) = params.delete_filter {
database_editor
.modify_view_filters(&params.view_id, payload.into())
.await?;
}
if let Some(update_sort) = params.update_sort {

View File

@ -36,8 +36,8 @@ impl CalculationsService {
let mut sum = 0.0;
let mut len = 0.0;
let field_type = FieldType::from(field.field_type);
if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
.get_type_option_cell_data_handler(&field_type)
if let Some(handler) =
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type)
{
for row_cell in row_cells {
if let Some(cell) = &row_cell.cell {
@ -131,8 +131,8 @@ impl CalculationsService {
fn calculate_count_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
let field_type = FieldType::from(field.field_type);
if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
.get_type_option_cell_data_handler(&field_type)
if let Some(handler) =
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type)
{
if !row_cells.is_empty() {
return format!(
@ -154,8 +154,8 @@ impl CalculationsService {
fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String {
let field_type = FieldType::from(field.field_type);
if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
.get_type_option_cell_data_handler(&field_type)
if let Some(handler) =
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type)
{
if !row_cells.is_empty() {
return format!(
@ -183,8 +183,8 @@ impl CalculationsService {
let mut values = vec![];
let field_type = FieldType::from(field.field_type);
if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None)
.get_type_option_cell_data_handler(&field_type)
if let Some(handler) =
TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type)
{
for row_cell in row_cells {
if let Some(cell) = &row_cell.cell {

View File

@ -4,4 +4,3 @@ use std::sync::Arc;
use crate::utils::cache::AnyTypeCache;
pub type CellCache = Arc<RwLock<AnyTypeCache<u64>>>;
pub type CellFilterCache = Arc<RwLock<AnyTypeCache<String>>>;

View File

@ -75,7 +75,7 @@ pub fn apply_cell_changeset(
cell_data_cache: Option<CellCache>,
) -> Result<Cell, FlowyError> {
let field_type = FieldType::from(field.field_type);
match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
match TypeOptionCellExt::new(field, cell_data_cache)
.get_type_option_cell_data_handler(&field_type)
{
None => Ok(Cell::default()),
@ -128,7 +128,7 @@ pub fn try_decode_cell_to_cell_protobuf(
field: &Field,
cell_data_cache: Option<CellCache>,
) -> FlowyResult<CellProtobufBlob> {
match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
match TypeOptionCellExt::new(field, cell_data_cache)
.get_type_option_cell_data_handler(to_field_type)
{
None => Ok(CellProtobufBlob::default()),
@ -143,7 +143,7 @@ pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
field: &Field,
cell_data_cache: Option<CellCache>,
) -> Option<T> {
let handler = TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
let handler = TypeOptionCellExt::new(field, cell_data_cache)
.get_type_option_cell_data_handler(to_field_type)?;
handler
.get_cell_data(cell, from_field_type, field)
@ -152,8 +152,8 @@ pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
}
/// Returns a string that represents the current field_type's cell data.
/// For example, The string of the Multi-Select cell will be a list of the option's name
/// separated by a comma.
/// For example, a Multi-Select cell will be represented by a list of the options' names
/// separated by commas.
///
/// # Arguments
///
@ -162,16 +162,13 @@ pub fn try_decode_cell_to_cell_data<T: Default + 'static>(
/// * `from_field_type`: the original field type of the passed-in cell data.
/// * `field`: used to get the corresponding TypeOption for the specified field type.
///
/// returns: String
pub fn stringify_cell_data(
cell: &Cell,
to_field_type: &FieldType,
from_field_type: &FieldType,
field: &Field,
) -> String {
match TypeOptionCellExt::new_with_cell_data_cache(field, None)
.get_type_option_cell_data_handler(from_field_type)
{
match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(from_field_type) {
None => "".to_string(),
Some(handler) => handler.handle_stringify_cell(cell, to_field_type, field),
}

View File

@ -4,7 +4,9 @@ use std::sync::Arc;
use collab_database::database::MutexDatabase;
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId};
use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, OrderObjectPosition};
use collab_database::views::{
DatabaseLayout, DatabaseView, FilterMap, LayoutSetting, OrderObjectPosition,
};
use futures::StreamExt;
use lib_infra::box_any::BoxAny;
use tokio::sync::{broadcast, RwLock};
@ -32,7 +34,7 @@ use crate::services::field::{
use crate::services::field_settings::{
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
};
use crate::services::filter::Filter;
use crate::services::filter::{Filter, FilterChangeset};
use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset};
use crate::services::share::csv::{CSVExport, CSVFormat};
use crate::services::sort::Sort;
@ -214,16 +216,13 @@ impl DatabaseEditor {
Ok(())
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub async fn create_or_update_filter(&self, params: UpdateFilterParams) -> FlowyResult<()> {
let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
view_editor.v_insert_filter(params).await?;
Ok(())
}
pub async fn delete_filter(&self, params: DeleteFilterPayloadPB) -> FlowyResult<()> {
let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
view_editor.v_delete_filter(params).await?;
pub async fn modify_view_filters(
&self,
view_id: &str,
changeset: FilterChangeset,
) -> FlowyResult<()> {
let view_editor = self.database_views.get_view_editor(view_id).await?;
view_editor.v_modify_filters(changeset).await?;
Ok(())
}
@ -267,7 +266,8 @@ impl DatabaseEditor {
pub async fn get_all_filters(&self, view_id: &str) -> RepeatedFilterPB {
if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await {
view_editor.v_get_all_filters().await.into()
let filters = view_editor.v_get_all_filters().await;
RepeatedFilterPB::from(&filters)
} else {
RepeatedFilterPB { items: vec![] }
}
@ -1259,10 +1259,9 @@ impl DatabaseEditor {
row_ids: Option<&Vec<String>>,
) -> FlowyResult<Vec<RelatedRowDataPB>> {
let primary_field = self.database.lock().fields.get_primary_field().unwrap();
let handler =
TypeOptionCellExt::new_with_cell_data_cache(&primary_field, Some(self.cell_cache.clone()))
.get_type_option_cell_data_handler(&FieldType::RichText)
.ok_or(FlowyError::internal())?;
let handler = TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone()))
.get_type_option_cell_data_handler(&FieldType::RichText)
.ok_or(FlowyError::internal())?;
let row_data = {
let database = self.database.lock();
@ -1566,13 +1565,12 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
.get_calculation::<Calculation>(view_id, field_id)
}
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>> {
fn get_all_filters(&self, view_id: &str) -> Vec<Filter> {
self
.database
.lock()
.get_all_filters(view_id)
.into_iter()
.map(Arc::new)
.collect()
}
@ -1581,7 +1579,14 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
}
fn insert_filter(&self, view_id: &str, filter: Filter) {
self.database.lock().insert_filter(view_id, filter);
self.database.lock().insert_filter(view_id, &filter);
}
fn save_filters(&self, view_id: &str, filters: &[Filter]) {
self
.database
.lock()
.save_filters::<Filter, FilterMap>(view_id, filters);
}
fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter> {
@ -1591,15 +1596,6 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
.get_filter::<Filter>(view_id, filter_id)
}
fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option<Filter> {
self
.database
.lock()
.get_all_filters::<Filter>(view_id)
.into_iter()
.find(|filter| filter.field_id == field_id)
}
fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option<LayoutSetting> {
self.database.lock().get_layout_setting(view_id, layout_ty)
}
@ -1632,7 +1628,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl {
field: &Field,
field_type: &FieldType,
) -> Option<Box<dyn TypeOptionCellDataHandler>> {
TypeOptionCellExt::new_with_cell_data_cache(field, Some(self.cell_cache.clone()))
TypeOptionCellExt::new(field, Some(self.cell_cache.clone()))
.get_type_option_cell_data_handler(field_type)
}

View File

@ -2,9 +2,7 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use collab_database::database::{
gen_database_calculation_id, gen_database_filter_id, gen_database_sort_id,
};
use collab_database::database::{gen_database_calculation_id, gen_database_sort_id};
use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cells, Row, RowDetail, RowId};
use collab_database::views::{DatabaseLayout, DatabaseView};
@ -15,11 +13,10 @@ use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::af_spawn;
use crate::entities::{
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterPayloadPB,
DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB,
LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, ReorderSortPayloadPB,
RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB,
UpdateFilterParams, UpdateSortPayloadPB,
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteSortPayloadPB, FieldType,
FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, LayoutSettingChangeset,
LayoutSettingParams, RemoveCalculationChangesetPB, ReorderSortPayloadPB, RowMetaPB, RowsChangePB,
SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, UpdateSortPayloadPB,
};
use crate::notification::{send_notification, DatabaseNotification};
use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController};
@ -37,9 +34,7 @@ use crate::services::database_view::{
DatabaseViewChangedNotifier, DatabaseViewChangedReceiverRunner,
};
use crate::services::field_settings::FieldSettings;
use crate::services::filter::{
Filter, FilterChangeset, FilterContext, FilterController, UpdatedFilter,
};
use crate::services::filter::{Filter, FilterChangeset, FilterController};
use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowContext, RowChangeset};
use crate::services::setting::CalendarLayoutSetting;
use crate::services::sort::{Sort, SortChangeset, SortController};
@ -618,73 +613,28 @@ impl DatabaseViewEditor {
Ok(())
}
pub async fn v_get_all_filters(&self) -> Vec<Arc<Filter>> {
pub async fn v_get_all_filters(&self) -> Vec<Filter> {
self.delegate.get_all_filters(&self.view_id)
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_insert_filter(&self, params: UpdateFilterParams) -> FlowyResult<()> {
let is_exist = params.filter_id.is_some();
let filter_id = match params.filter_id {
None => gen_database_filter_id(),
Some(filter_id) => filter_id,
};
let filter = Filter {
id: filter_id.clone(),
field_id: params.field_id.clone(),
field_type: params.field_type,
condition: params.condition,
content: params.content,
};
let filter_controller = self.filter_controller.clone();
let changeset = if is_exist {
let old_filter = self.delegate.get_filter(&self.view_id, &filter.id);
self.delegate.insert_filter(&self.view_id, filter.clone());
filter_controller
.did_receive_changes(FilterChangeset::from_update(UpdatedFilter::new(
old_filter, filter,
)))
.await
} else {
self.delegate.insert_filter(&self.view_id, filter.clone());
filter_controller
.did_receive_changes(FilterChangeset::from_insert(filter))
.await
};
drop(filter_controller);
if let Some(changeset) = changeset {
notify_did_update_filter(changeset).await;
}
Ok(())
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_delete_filter(&self, params: DeleteFilterPayloadPB) -> FlowyResult<()> {
let filter_context = FilterContext {
filter_id: params.filter_id.clone(),
field_id: params.field_id.clone(),
field_type: params.field_type,
};
let changeset = self
.filter_controller
.did_receive_changes(FilterChangeset::from_delete(filter_context.clone()))
.await;
self
.delegate
.delete_filter(&self.view_id, &params.filter_id);
if changeset.is_some() {
notify_did_update_filter(changeset.unwrap()).await;
}
Ok(())
}
pub async fn v_get_filter(&self, filter_id: &str) -> Option<Filter> {
self.delegate.get_filter(&self.view_id, filter_id)
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_modify_filters(&self, changeset: FilterChangeset) -> FlowyResult<()> {
let filter_controller = self.filter_controller.clone();
// self.delegate.insert_filter(&self.view_id, filter.clone());
let notification = filter_controller.apply_changeset(changeset).await;
drop(filter_controller);
notify_did_update_filter(notification).await;
Ok(())
}
/// Returns the current calendar settings
#[tracing::instrument(level = "trace", skip(self))]
pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams {
@ -830,25 +780,13 @@ impl DatabaseViewEditor {
})
.await;
if let Some(filter) = self
.delegate
.get_filter_by_field_id(&self.view_id, field_id)
{
let old = Filter {
field_type: FieldType::from(old_field.field_type),
..filter.clone()
};
let updated_filter = UpdatedFilter::new(Some(old), filter);
let filter_changeset = FilterChangeset::from_update(updated_filter);
if old_field.field_type != field.field_type {
let filter_controller = self.filter_controller.clone();
af_spawn(async move {
if let Some(notification) = filter_controller
.did_receive_changes(filter_changeset)
.await
{
notify_did_update_filter(notification).await;
}
});
let changeset = FilterChangeset::DeleteAllWithFieldId {
field_id: field.id.clone(),
};
let notification = filter_controller.apply_changeset(changeset).await;
notify_did_update_filter(notification).await;
}
}
Ok(())

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::rows::{RowDetail, RowId};
use lib_infra::future::{to_fut, Fut};
use lib_infra::future::Fut;
use crate::services::cell::CellCache;
use crate::services::database_view::{
@ -46,11 +46,6 @@ pub async fn make_filter_controller(
struct DatabaseViewFilterDelegateImpl(Arc<dyn DatabaseViewOperation>);
impl FilterDelegate for DatabaseViewFilterDelegateImpl {
fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut<Option<Arc<Filter>>> {
let filter = self.0.get_filter(view_id, filter_id).map(Arc::new);
to_fut(async move { filter })
}
fn get_field(&self, field_id: &str) -> Option<Field> {
self.0.get_field(field_id)
}
@ -66,4 +61,8 @@ impl FilterDelegate for DatabaseViewFilterDelegateImpl {
fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>> {
self.0.get_row(view_id, rows_id)
}
fn save_filters(&self, view_id: &str, filters: &[Filter]) {
self.0.save_filters(view_id, filters)
}
}

View File

@ -91,15 +91,15 @@ pub trait DatabaseViewOperation: Send + Sync + 'static {
fn remove_calculation(&self, view_id: &str, calculation_id: &str);
fn get_all_filters(&self, view_id: &str) -> Vec<Arc<Filter>>;
fn get_all_filters(&self, view_id: &str) -> Vec<Filter>;
fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter>;
fn delete_filter(&self, view_id: &str, filter_id: &str);
fn insert_filter(&self, view_id: &str, filter: Filter);
fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter>;
fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option<Filter>;
fn save_filters(&self, view_id: &str, filters: &[Filter]);
fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option<LayoutSetting>;

View File

@ -177,7 +177,7 @@ impl TypeOptionCellDataFilter for TimestampTypeOption {
_filter: &<Self as TypeOption>::CellFilter,
_cell_data: &<Self as TypeOption>::CellData,
) -> bool {
false
true
}
}

View File

@ -19,7 +19,7 @@ use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
};
use crate::services::filter::FromFilterString;
use crate::services::filter::ParseFilterData;
use crate::services::sort::SortCondition;
pub trait TypeOption {
@ -58,7 +58,7 @@ pub trait TypeOption {
type CellProtobufType: TryInto<Bytes, Error = ProtobufError> + Debug;
/// Represents the filter configuration for this type option.
type CellFilter: FromFilterString + Send + Sync + 'static;
type CellFilter: ParseFilterData + Clone + Send + Sync + 'static;
}
/// This trait providing serialization and deserialization methods for cell data.
///

View File

@ -9,9 +9,7 @@ use flowy_error::FlowyResult;
use lib_infra::box_any::BoxAny;
use crate::entities::FieldType;
use crate::services::cell::{
CellCache, CellDataChangeset, CellDataDecoder, CellFilterCache, CellProtobufBlob,
};
use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob};
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
@ -55,7 +53,7 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static {
sort_condition: SortCondition,
) -> Ordering;
fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool;
fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool;
/// Format the cell to string using the passed-in [FieldType] and [Field].
/// The [Cell] is generic, so we need to know the [FieldType] and [Field] to format the cell.
@ -100,7 +98,6 @@ impl AsRef<u64> for CellDataCacheKey {
struct TypeOptionCellDataHandlerImpl<T> {
inner: T,
cell_data_cache: Option<CellCache>,
cell_filter_cache: Option<CellFilterCache>,
}
impl<T> TypeOptionCellDataHandlerImpl<T>
@ -122,13 +119,11 @@ where
pub fn new_with_boxed(
inner: T,
cell_filter_cache: Option<CellFilterCache>,
cell_data_cache: Option<CellCache>,
) -> Box<dyn TypeOptionCellDataHandler> {
Self {
inner,
cell_data_cache,
cell_filter_cache,
}
.into_boxed()
}
@ -308,11 +303,11 @@ where
}
}
fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool {
fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool {
let perform_filter = || {
let filter_cache = self.cell_filter_cache.as_ref()?.read();
let cell_filter = filter_cache.get::<<Self as TypeOption>::CellFilter>(&field.id)?;
let cell_data = self.get_decoded_cell_data(cell, field_type, field).ok()?;
let field_type = FieldType::from(field.field_type);
let cell_filter = filter.downcast_ref::<<Self as TypeOption>::CellFilter>()?;
let cell_data = self.get_decoded_cell_data(cell, &field_type, field).ok()?;
Some(self.apply_filter(cell_filter, &cell_data))
};
@ -362,28 +357,16 @@ where
pub struct TypeOptionCellExt<'a> {
field: &'a Field,
cell_data_cache: Option<CellCache>,
cell_filter_cache: Option<CellFilterCache>,
}
impl<'a> TypeOptionCellExt<'a> {
pub fn new_with_cell_data_cache(field: &'a Field, cell_data_cache: Option<CellCache>) -> Self {
pub fn new(field: &'a Field, cell_data_cache: Option<CellCache>) -> Self {
Self {
field,
cell_data_cache,
cell_filter_cache: None,
}
}
pub fn new(
field: &'a Field,
cell_data_cache: Option<CellCache>,
cell_filter_cache: Option<CellFilterCache>,
) -> Self {
let mut this = Self::new_with_cell_data_cache(field, cell_data_cache);
this.cell_filter_cache = cell_filter_cache;
this
}
pub fn get_cells<T>(&self) -> Vec<T> {
let field_type = FieldType::from(self.field.field_type);
match self.get_type_option_cell_data_handler(&field_type) {
@ -403,103 +386,63 @@ impl<'a> TypeOptionCellExt<'a> {
.field
.get_type_option::<RichTextTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::Number => self
.field
.get_type_option::<NumberTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::DateTime => self
.field
.get_type_option::<DateTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::LastEditedTime | FieldType::CreatedTime => self
.field
.get_type_option::<TimestampTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::SingleSelect => self
.field
.get_type_option::<SingleSelectTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::MultiSelect => self
.field
.get_type_option::<MultiSelectTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::Checkbox => self
.field
.get_type_option::<CheckboxTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::URL => {
self
.field
.get_type_option::<URLTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
})
},
FieldType::Checklist => self
.field
.get_type_option::<ChecklistTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
FieldType::Relation => self
.field
.get_type_option::<RelationTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone())
}),
}
}

View File

@ -2,8 +2,9 @@ use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use collab_database::database::gen_database_filter_id;
use collab_database::fields::Field;
use collab_database::rows::{Cell, Row, RowDetail, RowId};
use collab_database::rows::{Row, RowDetail, RowId};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
@ -14,33 +15,26 @@ use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatch
use crate::entities::filter_entities::*;
use crate::entities::{FieldType, InsertedRowPB, RowMetaPB};
use crate::services::cell::{CellCache, CellFilterCache};
use crate::services::cell::CellCache;
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier};
use crate::services::field::*;
use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification};
use crate::utils::cache::AnyTypeCache;
use crate::services::field::TypeOptionCellExt;
use crate::services::filter::{Filter, FilterChangeset, FilterInner, FilterResultNotification};
pub trait FilterDelegate: Send + Sync + 'static {
fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut<Option<Arc<Filter>>>;
fn get_field(&self, field_id: &str) -> Option<Field>;
fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Arc<Field>>>;
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>;
fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>>;
}
pub trait FromFilterString {
fn from_filter(filter: &Filter) -> Self
where
Self: Sized;
fn save_filters(&self, view_id: &str, filters: &[Filter]);
}
pub struct FilterController {
view_id: String,
handler_id: String,
delegate: Box<dyn FilterDelegate>,
result_by_row_id: DashMap<RowId, FilterResult>,
result_by_row_id: DashMap<RowId, bool>,
cell_cache: CellCache,
cell_filter_cache: CellFilterCache,
filters: RwLock<Vec<Filter>>,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
notifier: DatabaseViewChangedNotifier,
}
@ -57,26 +51,23 @@ impl FilterController {
handler_id: &str,
delegate: T,
task_scheduler: Arc<RwLock<TaskDispatcher>>,
filters: Vec<Arc<Filter>>,
filters: Vec<Filter>,
cell_cache: CellCache,
notifier: DatabaseViewChangedNotifier,
) -> Self
where
T: FilterDelegate + 'static,
{
let this = Self {
Self {
view_id: view_id.to_string(),
handler_id: handler_id.to_string(),
delegate: Box::new(delegate),
result_by_row_id: DashMap::default(),
cell_cache,
// Cache by field_id
cell_filter_cache: AnyTypeCache::<String>::new(),
filters: RwLock::new(filters),
task_scheduler,
notifier,
};
this.refresh_filters(filters).await;
this
}
}
pub async fn close(&self) {
@ -100,7 +91,9 @@ impl FilterController {
}
pub async fn filter_rows(&self, rows: &mut Vec<Arc<RowDetail>>) {
if self.cell_filter_cache.read().is_empty() {
let filters = self.filters.read().await;
if filters.is_empty() {
return;
}
let field_by_field_id = self.get_field_map().await;
@ -110,7 +103,7 @@ impl FilterController {
&self.result_by_row_id,
&field_by_field_id,
&self.cell_cache,
&self.cell_filter_cache,
&filters,
);
});
@ -118,7 +111,7 @@ impl FilterController {
self
.result_by_row_id
.get(&row_detail.row.id)
.map(|result| result.is_visible())
.map(|result| *result)
.unwrap_or(false)
});
}
@ -144,12 +137,14 @@ impl FilterController {
let event_type = FilterEvent::from_str(predicate).unwrap();
match event_type {
FilterEvent::FilterDidChanged => self.filter_all_rows().await?,
FilterEvent::RowDidChanged(row_id) => self.filter_row(row_id).await?,
FilterEvent::RowDidChanged(row_id) => self.filter_single_row(row_id).await?,
}
Ok(())
}
async fn filter_row(&self, row_id: RowId) -> FlowyResult<()> {
async fn filter_single_row(&self, row_id: RowId) -> FlowyResult<()> {
let filters = self.filters.read().await;
if let Some((_, row_detail)) = self.delegate.get_row(&self.view_id, &row_id).await {
let field_by_field_id = self.get_field_map().await;
let mut notification = FilterResultNotification::new(self.view_id.clone());
@ -158,7 +153,7 @@ impl FilterController {
&self.result_by_row_id,
&field_by_field_id,
&self.cell_cache,
&self.cell_filter_cache,
&filters,
) {
if is_visible {
if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await {
@ -179,6 +174,8 @@ impl FilterController {
}
async fn filter_all_rows(&self) -> FlowyResult<()> {
let filters = self.filters.read().await;
let field_by_field_id = self.get_field_map().await;
let mut visible_rows = vec![];
let mut invisible_rows = vec![];
@ -195,7 +192,7 @@ impl FilterController {
&self.result_by_row_id,
&field_by_field_id,
&self.cell_cache,
&self.cell_filter_cache,
&filters,
) {
if is_visible {
let row_meta = RowMetaPB::from(row_detail.as_ref());
@ -211,15 +208,16 @@ impl FilterController {
invisible_rows,
visible_rows,
};
tracing::Span::current().record("filter_result", format!("{:?}", &notification).as_str());
tracing::trace!("filter result {:?}", filters);
let _ = self
.notifier
.send(DatabaseViewChanged::FilterNotification(notification));
Ok(())
}
pub async fn did_receive_row_changed(&self, row_id: RowId) {
if !self.cell_filter_cache.read().is_empty() {
if !self.filters.read().await.is_empty() {
self
.gen_task(
FilterEvent::RowDidChanged(row_id),
@ -230,206 +228,196 @@ impl FilterController {
}
#[tracing::instrument(level = "trace", skip(self))]
pub async fn did_receive_changes(
&self,
changeset: FilterChangeset,
) -> Option<FilterChangesetNotificationPB> {
let mut notification: Option<FilterChangesetNotificationPB> = None;
pub async fn apply_changeset(&self, changeset: FilterChangeset) -> FilterChangesetNotificationPB {
let mut filters = self.filters.write().await;
if let Some(filter_type) = &changeset.insert_filter {
if let Some(filter) = self.filter_from_filter_id(&filter_type.id).await {
notification = Some(FilterChangesetNotificationPB::from_insert(
&self.view_id,
vec![filter],
));
}
if let Some(filter) = self
.delegate
.get_filter(&self.view_id, &filter_type.id)
.await
{
self.refresh_filters(vec![filter]).await;
}
}
if let Some(updated_filter_type) = changeset.update_filter {
if let Some(old_filter_type) = updated_filter_type.old {
let new_filter = self
.filter_from_filter_id(&updated_filter_type.new.id)
.await;
let old_filter = self.filter_from_filter_id(&old_filter_type.id).await;
// Get the filter id
let mut filter_id = old_filter.map(|filter| filter.id);
if filter_id.is_none() {
filter_id = new_filter.as_ref().map(|filter| filter.id.clone());
match changeset {
FilterChangeset::Insert {
parent_filter_id,
data,
} => {
let new_filter = Filter {
id: gen_database_filter_id(),
inner: data,
};
match parent_filter_id {
Some(parent_filter_id) => {
if let Some(parent_filter) = filters
.iter_mut()
.find_map(|filter| filter.find_filter(&parent_filter_id))
{
// TODO(RS): error handling for inserting filters
let _result = parent_filter.insert_filter(new_filter);
}
},
None => {
filters.push(new_filter);
},
}
if let Some(filter_id) = filter_id {
// Update the corresponding filter in the cache
if let Some(filter) = self.delegate.get_filter(&self.view_id, &filter_id).await {
self.refresh_filters(vec![filter]).await;
},
FilterChangeset::UpdateType {
filter_id,
filter_type,
} => {
for filter in filters.iter_mut() {
let filter = filter.find_filter(&filter_id);
if let Some(filter) = filter {
let result = filter.convert_to_and_or_filter_type(filter_type);
if result.is_ok() {
break;
}
}
notification = Some(FilterChangesetNotificationPB::from_update(
&self.view_id,
vec![UpdatedFilter {
filter_id,
filter: new_filter,
}],
));
}
}
},
FilterChangeset::UpdateData { filter_id, data } => {
if let Some(filter) = filters
.iter_mut()
.find_map(|filter| filter.find_filter(&filter_id))
{
// TODO(RS): error handling for updating filter data
let _result = filter.update_filter_data(data);
}
},
FilterChangeset::Delete {
filter_id,
field_id: _,
} => {
for (position, filter) in filters.iter_mut().enumerate() {
if filter.id == filter_id {
filters.remove(position);
break;
}
let parent_filter = filter.find_parent_of_filter(&filter_id);
if let Some(filter) = parent_filter {
let result = filter.delete_filter(&filter_id);
if result.is_ok() {
break;
}
}
}
},
FilterChangeset::DeleteAllWithFieldId { field_id } => {
let mut filter_ids: Vec<String> = vec![];
for filter in filters.iter_mut() {
filter.find_all_filters_with_field_id(&field_id, &mut filter_ids);
}
for filter_id in filter_ids {
for (position, filter) in filters.iter_mut().enumerate() {
if filter.id == filter_id {
filters.remove(position);
break;
}
let parent_filter = filter.find_parent_of_filter(&filter_id);
if let Some(filter) = parent_filter {
let _ = filter.delete_filter(&filter_id);
}
}
}
},
}
if let Some(filter_context) = &changeset.delete_filter {
if let Some(filter) = self.filter_from_filter_id(&filter_context.filter_id).await {
notification = Some(FilterChangesetNotificationPB::from_delete(
&self.view_id,
vec![filter],
));
}
self
.cell_filter_cache
.write()
.remove(&filter_context.field_id);
}
self.delegate.save_filters(&self.view_id, &filters);
self
.gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background)
.await;
tracing::trace!("{:?}", notification);
notification
}
async fn filter_from_filter_id(&self, filter_id: &str) -> Option<FilterPB> {
self
.delegate
.get_filter(&self.view_id, filter_id)
.await
.map(|filter| FilterPB::from(filter.as_ref()))
}
#[tracing::instrument(level = "trace", skip_all)]
async fn refresh_filters(&self, filters: Vec<Arc<Filter>>) {
for filter in filters {
let field_id = &filter.field_id;
tracing::trace!("Create filter with type: {:?}", filter.field_type);
match &filter.field_type {
FieldType::RichText => {
self
.cell_filter_cache
.write()
.insert(field_id, TextFilterPB::from_filter(filter.as_ref()));
},
FieldType::Number => {
self
.cell_filter_cache
.write()
.insert(field_id, NumberFilterPB::from_filter(filter.as_ref()));
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
self
.cell_filter_cache
.write()
.insert(field_id, DateFilterPB::from_filter(filter.as_ref()));
},
FieldType::SingleSelect | FieldType::MultiSelect => {
self
.cell_filter_cache
.write()
.insert(field_id, SelectOptionFilterPB::from_filter(filter.as_ref()));
},
FieldType::Checkbox => {
self
.cell_filter_cache
.write()
.insert(field_id, CheckboxFilterPB::from_filter(filter.as_ref()));
},
FieldType::URL => {
self
.cell_filter_cache
.write()
.insert(field_id, TextFilterPB::from_filter(filter.as_ref()));
},
FieldType::Checklist => {
self
.cell_filter_cache
.write()
.insert(field_id, ChecklistFilterPB::from_filter(filter.as_ref()));
},
FieldType::Relation => {
self
.cell_filter_cache
.write()
.insert(field_id, RelationFilterPB::from_filter(filter.as_ref()));
},
}
}
FilterChangesetNotificationPB::from_filters(&self.view_id, &filters)
}
}
/// Returns None if there is no change in this row after applying the filter
/// Returns `Some` if the visibility of the row changed after applying the filter and `None`
/// otherwise
#[tracing::instrument(level = "trace", skip_all)]
fn filter_row(
row: &Row,
result_by_row_id: &DashMap<RowId, FilterResult>,
result_by_row_id: &DashMap<RowId, bool>,
field_by_field_id: &HashMap<String, Arc<Field>>,
cell_data_cache: &CellCache,
cell_filter_cache: &CellFilterCache,
filters: &Vec<Filter>,
) -> Option<(RowId, bool)> {
// Create a filter result cache if it's not exist
let mut filter_result = result_by_row_id.entry(row.id.clone()).or_default();
let old_is_visible = filter_result.is_visible();
// Create a filter result cache if it doesn't exist
let mut filter_result = result_by_row_id.entry(row.id.clone()).or_insert(true);
let old_is_visible = *filter_result;
// Iterate each cell of the row to check its visibility
for (field_id, field) in field_by_field_id {
if !cell_filter_cache.read().contains(field_id) {
filter_result.visible_by_field_id.remove(field_id);
continue;
}
let cell = row.cells.get(field_id).cloned();
let field_type = FieldType::from(field.field_type);
// if the visibility of the cell_rew is changed, which means the visibility of the
// row is changed too.
if let Some(is_visible) =
filter_cell(&field_type, field, cell, cell_data_cache, cell_filter_cache)
{
filter_result
.visible_by_field_id
.insert(field_id.to_string(), is_visible);
let mut new_is_visible = true;
for filter in filters {
if let Some(is_visible) = apply_filter(row, field_by_field_id, cell_data_cache, filter) {
new_is_visible = new_is_visible && is_visible;
}
}
let is_visible = filter_result.is_visible();
if old_is_visible != is_visible {
Some((row.id.clone(), is_visible))
*filter_result = new_is_visible;
if old_is_visible != new_is_visible {
Some((row.id.clone(), new_is_visible))
} else {
None
}
}
// Returns None if there is no change in this cell after applying the filter
// Returns Some if the visibility of the cell is changed
#[tracing::instrument(level = "trace", skip_all, fields(cell_content))]
fn filter_cell(
field_type: &FieldType,
field: &Arc<Field>,
cell: Option<Cell>,
/// Recursively applies a `Filter` to a `Row`'s cells.
fn apply_filter(
row: &Row,
field_by_field_id: &HashMap<String, Arc<Field>>,
cell_data_cache: &CellCache,
cell_filter_cache: &CellFilterCache,
filter: &Filter,
) -> Option<bool> {
let handler = TypeOptionCellExt::new(
field.as_ref(),
Some(cell_data_cache.clone()),
Some(cell_filter_cache.clone()),
)
.get_type_option_cell_data_handler(field_type)?;
let is_visible =
handler.handle_cell_filter(field_type, field.as_ref(), &cell.unwrap_or_default());
Some(is_visible)
match &filter.inner {
FilterInner::And { children } => {
if children.is_empty() {
return None;
}
for child_filter in children.iter() {
if let Some(false) = apply_filter(row, field_by_field_id, cell_data_cache, child_filter) {
return Some(false);
}
}
Some(true)
},
FilterInner::Or { children } => {
if children.is_empty() {
return None;
}
for child_filter in children.iter() {
if let Some(true) = apply_filter(row, field_by_field_id, cell_data_cache, child_filter) {
return Some(true);
}
}
Some(false)
},
FilterInner::Data {
field_id,
field_type,
condition_and_content,
} => {
let field = match field_by_field_id.get(field_id) {
Some(field) => field,
None => {
tracing::error!("cannot find field");
return Some(false);
},
};
if *field_type != FieldType::from(field.field_type) {
tracing::error!("field type of filter doesn't match field type of field");
return Some(false);
}
let cell = row.cells.get(field_id).cloned();
let field_type = FieldType::from(field.field_type);
if let Some(handler) = TypeOptionCellExt::new(field.as_ref(), Some(cell_data_cache.clone()))
.get_type_option_cell_data_handler(&field_type)
{
Some(handler.handle_cell_filter(
field.as_ref(),
&cell.unwrap_or_default(),
condition_and_content,
))
} else {
Some(true)
}
},
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -446,6 +434,7 @@ impl ToString for FilterEvent {
impl FromStr for FilterEvent {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}

View File

@ -1,125 +1,397 @@
use std::mem;
use anyhow::bail;
use collab::core::any_map::AnyMapExtension;
use collab_database::database::gen_database_filter_id;
use collab_database::rows::RowId;
use collab_database::views::{FilterMap, FilterMapBuilder};
use flowy_error::{FlowyError, FlowyResult};
use lib_infra::box_any::BoxAny;
use crate::entities::{FieldType, FilterPB, InsertedRowPB};
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType, FilterType,
InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
};
use crate::services::field::SelectOptionIds;
#[derive(Debug, Clone)]
pub trait ParseFilterData {
fn parse(condition: u8, content: String) -> Self;
}
#[derive(Debug)]
pub struct Filter {
pub id: String,
pub field_id: String,
pub field_type: FieldType,
pub condition: i64,
pub content: String,
pub inner: FilterInner,
}
impl Filter {
/// Recursively determine whether there are any data filters in the filter tree.
pub fn is_empty(&self) -> bool {
match &self.inner {
FilterInner::And { children } | FilterInner::Or { children } => children
.iter()
.map(|filter| filter.is_empty())
.all(|is_empty| is_empty),
FilterInner::Data { .. } => false,
}
}
pub fn find_filter(&mut self, filter_id: &str) -> Option<&mut Self> {
if self.id == filter_id {
return Some(self);
}
match &mut self.inner {
FilterInner::And { children } | FilterInner::Or { children } => {
for child_filter in children.iter_mut() {
let result = child_filter.find_filter(filter_id);
if result.is_some() {
return result;
}
}
None
},
FilterInner::Data { .. } => None,
}
}
pub fn find_parent_of_filter(&mut self, filter_id: &str) -> Option<&mut Self> {
if self.id == filter_id {
return None;
}
match &mut self.inner {
FilterInner::And { children } | FilterInner::Or { children } => {
for child_filter in children.iter_mut() {
if child_filter.id == filter_id {
return Some(child_filter);
}
let result = child_filter.find_parent_of_filter(filter_id);
if result.is_some() {
return result;
}
}
None
},
FilterInner::Data { .. } => None,
}
}
/// converts a filter from And/Or/Data to And/Or. If the current type of the filter is Data,
/// return the FilterInner after the conversion.
pub fn convert_to_and_or_filter_type(
&mut self,
filter_type: FilterType,
) -> FlowyResult<Option<FilterInner>> {
match (&mut self.inner, filter_type) {
(FilterInner::And { children }, FilterType::Or) => {
self.inner = FilterInner::Or {
children: mem::take(children),
};
Ok(None)
},
(FilterInner::Or { children }, FilterType::And) => {
self.inner = FilterInner::And {
children: mem::take(children),
};
Ok(None)
},
(FilterInner::Data { .. }, FilterType::And) => {
let mut inner = FilterInner::And { children: vec![] };
mem::swap(&mut self.inner, &mut inner);
Ok(Some(inner))
},
(FilterInner::Data { .. }, FilterType::Or) => {
let mut inner = FilterInner::Or { children: vec![] };
mem::swap(&mut self.inner, &mut inner);
Ok(Some(inner))
},
(_, FilterType::Data) => {
// from And/Or to Data
Err(FlowyError::internal().with_context(format!(
"conversion from {:?} to FilterType::Data not supported",
FilterType::from(&self.inner)
)))
},
_ => {
tracing::warn!("conversion to the same filter type");
Ok(None)
},
}
}
pub fn insert_filter(&mut self, filter: Filter) -> FlowyResult<()> {
match &mut self.inner {
FilterInner::And { children } | FilterInner::Or { children } => {
children.push(filter);
},
FilterInner::Data { .. } => {
// convert to FilterInner::And by default
let old_filter = self
.convert_to_and_or_filter_type(FilterType::And)
.and_then(|result| {
result.ok_or_else(|| FlowyError::internal().with_context("failed to convert filter"))
})?;
self.insert_filter(Filter {
id: gen_database_filter_id(),
inner: old_filter,
})?;
self.insert_filter(filter)?;
},
}
Ok(())
}
pub fn update_filter_data(&mut self, filter_data: FilterInner) -> FlowyResult<()> {
match &self.inner {
FilterInner::And { .. } | FilterInner::Or { .. } => Err(FlowyError::internal().with_context(
format!("unexpected filter type {:?}", FilterType::from(&self.inner)),
)),
_ => {
self.inner = filter_data;
Ok(())
},
}
}
pub fn delete_filter(&mut self, filter_id: &str) -> FlowyResult<()> {
match &mut self.inner {
FilterInner::And { children } | FilterInner::Or { children } => children
.iter()
.position(|filter| filter.id == filter_id)
.map(|position| {
children.remove(position);
})
.ok_or_else(|| {
FlowyError::internal()
.with_context(format!("filter with filter_id {:?} not found", filter_id))
}),
FilterInner::Data { .. } => Err(
FlowyError::internal().with_context("unexpected parent filter type of FilterInner::Data"),
),
}
}
pub fn find_all_filters_with_field_id(&mut self, matching_field_id: &str, ids: &mut Vec<String>) {
match &mut self.inner {
FilterInner::And { children } | FilterInner::Or { children } => {
for child_filter in children.iter_mut() {
child_filter.find_all_filters_with_field_id(matching_field_id, ids);
}
},
FilterInner::Data {
field_id,
field_type: _,
condition_and_content: _,
} => {
if field_id == matching_field_id {
ids.push(self.id.clone());
}
},
}
}
}
#[derive(Debug)]
pub enum FilterInner {
And {
children: Vec<Filter>,
},
Or {
children: Vec<Filter>,
},
Data {
field_id: String,
field_type: FieldType,
condition_and_content: BoxAny,
},
}
impl FilterInner {
pub fn new_data(
field_id: String,
field_type: FieldType,
condition: i64,
content: String,
) -> Self {
let condition_and_content = match field_type {
FieldType::RichText | FieldType::URL => {
BoxAny::new(TextFilterPB::parse(condition as u8, content))
},
FieldType::Number => BoxAny::new(NumberFilterPB::parse(condition as u8, content)),
FieldType::DateTime | FieldType::CreatedTime | FieldType::LastEditedTime => {
BoxAny::new(DateFilterPB::parse(condition as u8, content))
},
FieldType::SingleSelect | FieldType::MultiSelect => {
BoxAny::new(SelectOptionFilterPB::parse(condition as u8, content))
},
FieldType::Checklist => BoxAny::new(ChecklistFilterPB::parse(condition as u8, content)),
FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)),
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
};
FilterInner::Data {
field_id,
field_type,
condition_and_content,
}
}
pub fn get_int_repr(&self) -> i64 {
match self {
FilterInner::And { .. } => FILTER_AND_INDEX,
FilterInner::Or { .. } => FILTER_OR_INDEX,
FilterInner::Data { .. } => FILTER_DATA_INDEX,
}
}
}
const FILTER_ID: &str = "id";
const FILTER_TYPE: &str = "filter_type";
const FIELD_ID: &str = "field_id";
const FIELD_TYPE: &str = "ty";
const FILTER_CONDITION: &str = "condition";
const FILTER_CONTENT: &str = "content";
const FILTER_CHILDREN: &str = "children";
impl From<Filter> for FilterMap {
fn from(data: Filter) -> Self {
FilterMapBuilder::new()
.insert_str_value(FILTER_ID, data.id)
.insert_str_value(FIELD_ID, data.field_id)
.insert_str_value(FILTER_CONTENT, data.content)
.insert_i64_value(FIELD_TYPE, data.field_type.into())
.insert_i64_value(FILTER_CONDITION, data.condition)
.build()
const FILTER_AND_INDEX: i64 = 0;
const FILTER_OR_INDEX: i64 = 1;
const FILTER_DATA_INDEX: i64 = 2;
impl<'a> From<&'a Filter> for FilterMap {
fn from(filter: &'a Filter) -> Self {
let mut builder = FilterMapBuilder::new()
.insert_str_value(FILTER_ID, &filter.id)
.insert_i64_value(FILTER_TYPE, filter.inner.get_int_repr());
builder = match &filter.inner {
FilterInner::And { children } | FilterInner::Or { children } => {
builder.insert_maps(FILTER_CHILDREN, children.iter().collect::<Vec<&Filter>>())
},
FilterInner::Data {
field_id,
field_type,
condition_and_content,
} => {
let get_raw_condition_and_content = || -> Option<(u8, String)> {
let (condition, content) = match field_type {
FieldType::RichText | FieldType::URL => {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
FieldType::Number => {
let filter = condition_and_content.cloned::<NumberFilterPB>()?;
(filter.condition as u8, filter.content)
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
let filter = condition_and_content.cloned::<DateFilterPB>()?;
let content = DateFilterContentPB {
start: filter.start,
end: filter.end,
timestamp: filter.timestamp,
}
.to_string();
(filter.condition as u8, content)
},
FieldType::SingleSelect | FieldType::MultiSelect => {
let filter = condition_and_content.cloned::<SelectOptionFilterPB>()?;
let content = SelectOptionIds::from(filter.option_ids).to_string();
(filter.condition as u8, content)
},
FieldType::Checkbox => {
let filter = condition_and_content.cloned::<CheckboxFilterPB>()?;
(filter.condition as u8, "".to_string())
},
FieldType::Checklist => {
let filter = condition_and_content.cloned::<ChecklistFilterPB>()?;
(filter.condition as u8, "".to_string())
},
FieldType::Relation => {
let filter = condition_and_content.cloned::<RelationFilterPB>()?;
(filter.condition as u8, "".to_string())
},
};
Some((condition, content))
};
let (condition, content) = get_raw_condition_and_content().unwrap_or_else(|| {
tracing::error!("cannot deserialize filter condition and content filter properly");
Default::default()
});
builder
.insert_str_value(FIELD_ID, field_id)
.insert_i64_value(FIELD_TYPE, field_type.into())
.insert_i64_value(FILTER_CONDITION, condition as i64)
.insert_str_value(FILTER_CONTENT, content)
},
};
builder.build()
}
}
impl TryFrom<FilterMap> for Filter {
type Error = anyhow::Error;
fn try_from(filter: FilterMap) -> Result<Self, Self::Error> {
match (
filter.get_str_value(FILTER_ID),
filter.get_str_value(FIELD_ID),
) {
(Some(id), Some(field_id)) => {
let condition = filter.get_i64_value(FILTER_CONDITION).unwrap_or(0);
let content = filter.get_str_value(FILTER_CONTENT).unwrap_or_default();
let field_type = filter
.get_i64_value(FIELD_TYPE)
.map(FieldType::from)
.unwrap_or_default();
Ok(Filter {
id,
field_id,
field_type,
condition,
content,
})
fn try_from(filter_map: FilterMap) -> Result<Self, Self::Error> {
let filter_id = filter_map
.get_str_value(FILTER_ID)
.ok_or_else(|| anyhow::anyhow!("invalid filter data"))?;
let filter_type = filter_map
.get_i64_value(FILTER_TYPE)
.unwrap_or(FILTER_DATA_INDEX);
let filter = Filter {
id: filter_id,
inner: match filter_type {
FILTER_AND_INDEX => FilterInner::And {
children: filter_map.try_get_array(FILTER_CHILDREN),
},
FILTER_OR_INDEX => FilterInner::Or {
children: filter_map.try_get_array(FILTER_CHILDREN),
},
FILTER_DATA_INDEX => {
let field_id = filter_map
.get_str_value(FIELD_ID)
.ok_or_else(|| anyhow::anyhow!("invalid filter data"))?;
let field_type = filter_map
.get_i64_value(FIELD_TYPE)
.map(FieldType::from)
.unwrap_or_default();
let condition = filter_map.get_i64_value(FILTER_CONDITION).unwrap_or(0);
let content = filter_map.get_str_value(FILTER_CONTENT).unwrap_or_default();
FilterInner::new_data(field_id, field_type, condition, content)
},
_ => bail!("Unsupported filter type"),
},
_ => {
bail!("Invalid filter data")
},
}
};
Ok(filter)
}
}
#[derive(Debug)]
pub struct FilterChangeset {
pub(crate) insert_filter: Option<Filter>,
pub(crate) update_filter: Option<UpdatedFilter>,
pub(crate) delete_filter: Option<FilterContext>,
}
#[derive(Debug)]
pub struct UpdatedFilter {
pub old: Option<Filter>,
pub new: Filter,
}
impl UpdatedFilter {
pub fn new(old: Option<Filter>, new: Filter) -> UpdatedFilter {
Self { old, new }
}
}
impl FilterChangeset {
pub fn from_insert(filter: Filter) -> Self {
Self {
insert_filter: Some(filter),
update_filter: None,
delete_filter: None,
}
}
pub fn from_update(filter: UpdatedFilter) -> Self {
Self {
insert_filter: None,
update_filter: Some(filter),
delete_filter: None,
}
}
pub fn from_delete(filter_context: FilterContext) -> Self {
Self {
insert_filter: None,
update_filter: None,
delete_filter: Some(filter_context),
}
}
}
#[derive(Debug, Clone)]
pub struct FilterContext {
pub filter_id: String,
pub field_id: String,
pub field_type: FieldType,
}
impl From<&FilterPB> for FilterContext {
fn from(filter: &FilterPB) -> Self {
Self {
filter_id: filter.id.clone(),
field_id: filter.field_id.clone(),
field_type: filter.field_type,
}
}
pub enum FilterChangeset {
Insert {
parent_filter_id: Option<String>,
data: FilterInner,
},
UpdateType {
filter_id: String,
filter_type: FilterType,
},
UpdateData {
filter_id: String,
data: FilterInner,
},
Delete {
filter_id: String,
field_id: String,
},
DeleteAllWithFieldId {
field_id: String,
},
}
#[derive(Clone, Debug)]

View File

@ -1,7 +1,6 @@
use crate::services::filter::FilterController;
use lib_infra::future::BoxResultFuture;
use lib_infra::priority_task::{TaskContent, TaskHandler};
use std::collections::HashMap;
use std::sync::Arc;
pub struct FilterTaskHandler {
@ -40,21 +39,3 @@ impl TaskHandler for FilterTaskHandler {
})
}
}
/// Refresh the filter according to the field id.
#[derive(Default)]
pub(crate) struct FilterResult {
pub(crate) visible_by_field_id: HashMap<String, bool>,
}
impl FilterResult {
pub(crate) fn is_visible(&self) -> bool {
let mut is_visible = true;
for visible in self.visible_by_field_id.values() {
if !is_visible {
break;
}
is_visible = *visible;
}
is_visible
}
}

View File

@ -340,7 +340,7 @@ fn cmp_cell(
cell_data_cache: &CellCache,
sort_condition: SortCondition,
) -> Ordering {
match TypeOptionCellExt::new_with_cell_data_cache(field.as_ref(), Some(cell_data_cache.clone()))
match TypeOptionCellExt::new(field.as_ref(), Some(cell_data_cache.clone()))
.get_type_option_cell_data_handler(&field_type)
{
None => default_order(),

View File

@ -0,0 +1,314 @@
use bytes::Bytes;
use flowy_database2::entities::{
CheckboxFilterConditionPB, CheckboxFilterPB, DateFilterConditionPB, DateFilterPB, FieldType,
FilterDataPB, FilterPB, FilterType, NumberFilterConditionPB, NumberFilterPB,
};
use lib_infra::box_any::BoxAny;
use protobuf::ProtobufError;
use std::convert::TryInto;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged, FilterScript::*};
/// Create a single advanced filter:
///
/// 1. Add an OR filter
/// 2. Add a Checkbox and an AND filter to its children
/// 3. Add a DateTime and a Number filter to the AND filter's children
///
#[tokio::test]
async fn create_advanced_filter_test() {
let mut test = DatabaseFilterTest::new().await;
let create_checkbox_filter = || -> CheckboxFilterPB {
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::IsChecked,
}
};
let create_date_filter = || -> DateFilterPB {
DateFilterPB {
condition: DateFilterConditionPB::DateAfter,
timestamp: Some(1651366800),
..Default::default()
}
};
let create_number_filter = || -> NumberFilterPB {
NumberFilterPB {
condition: NumberFilterConditionPB::NumberIsNotEmpty,
content: "".to_string(),
}
};
let scripts = vec![
CreateOrFilter {
parent_filter_id: None,
changed: None,
},
Wait { millisecond: 100 },
AssertFilters {
expected: vec![FilterPB {
id: "".to_string(),
filter_type: FilterType::Or,
children: vec![],
data: None,
}],
},
];
test.run_scripts(scripts).await;
// OR
let or_filter = test.get_filter(FilterType::Or, None).await.unwrap();
let checkbox_filter_bytes: Result<Bytes, ProtobufError> = create_checkbox_filter().try_into();
let checkbox_filter_bytes = checkbox_filter_bytes.unwrap().to_vec();
let scripts = vec![
CreateDataFilter {
parent_filter_id: Some(or_filter.id.clone()),
field_type: FieldType::Checkbox,
data: BoxAny::new(create_checkbox_filter()),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 4,
}),
},
CreateAndFilter {
parent_filter_id: Some(or_filter.id),
changed: None,
},
Wait { millisecond: 100 },
AssertFilters {
expected: vec![FilterPB {
id: "".to_string(),
filter_type: FilterType::Or,
children: vec![
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::Checkbox,
data: checkbox_filter_bytes.clone(),
}),
},
FilterPB {
id: "".to_string(),
filter_type: FilterType::And,
children: vec![],
data: None,
},
],
data: None,
}],
},
AssertNumberOfVisibleRows { expected: 3 },
];
test.run_scripts(scripts).await;
// IS_CHECK OR AND
let and_filter = test.get_filter(FilterType::And, None).await.unwrap();
let date_filter_bytes: Result<Bytes, ProtobufError> = create_date_filter().try_into();
let date_filter_bytes = date_filter_bytes.unwrap().to_vec();
let number_filter_bytes: Result<Bytes, ProtobufError> = create_number_filter().try_into();
let number_filter_bytes = number_filter_bytes.unwrap().to_vec();
let scripts = vec![
CreateDataFilter {
parent_filter_id: Some(and_filter.id.clone()),
field_type: FieldType::DateTime,
data: BoxAny::new(create_date_filter()),
changed: None,
},
CreateDataFilter {
parent_filter_id: Some(and_filter.id),
field_type: FieldType::Number,
data: BoxAny::new(create_number_filter()),
changed: None,
},
Wait { millisecond: 100 },
AssertFilters {
expected: vec![FilterPB {
id: "".to_string(),
filter_type: FilterType::Or,
children: vec![
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::Checkbox,
data: checkbox_filter_bytes,
}),
},
FilterPB {
id: "".to_string(),
filter_type: FilterType::And,
children: vec![
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::DateTime,
data: date_filter_bytes,
}),
},
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::Number,
data: number_filter_bytes,
}),
},
],
data: None,
},
],
data: None,
}],
},
AssertNumberOfVisibleRows { expected: 4 },
];
test.run_scripts(scripts).await;
// IS_CHECK OR (DATE > 1651366800 AND NUMBER NOT EMPTY)
}
/// Create the same advanced filter single advanced filter:
///
/// 1. Add an OR filter
/// 2. Add a Checkbox and a DateTime filter to its children
/// 3. Add a Number filter to the DateTime filter's children
///
#[tokio::test]
async fn create_advanced_filter_with_conversion_test() {
let mut test = DatabaseFilterTest::new().await;
let create_checkbox_filter = || -> CheckboxFilterPB {
CheckboxFilterPB {
condition: CheckboxFilterConditionPB::IsChecked,
}
};
let create_date_filter = || -> DateFilterPB {
DateFilterPB {
condition: DateFilterConditionPB::DateAfter,
timestamp: Some(1651366800),
..Default::default()
}
};
let create_number_filter = || -> NumberFilterPB {
NumberFilterPB {
condition: NumberFilterConditionPB::NumberIsNotEmpty,
content: "".to_string(),
}
};
let scripts = vec![CreateOrFilter {
parent_filter_id: None,
changed: None,
}];
test.run_scripts(scripts).await;
// IS_CHECK OR DATE > 1651366800
let or_filter = test.get_filter(FilterType::Or, None).await.unwrap();
let scripts = vec![
CreateDataFilter {
parent_filter_id: Some(or_filter.id.clone()),
field_type: FieldType::Checkbox,
data: BoxAny::new(create_checkbox_filter()),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 4,
}),
},
CreateDataFilter {
parent_filter_id: Some(or_filter.id.clone()),
field_type: FieldType::DateTime,
data: BoxAny::new(create_date_filter()),
changed: None,
},
];
test.run_scripts(scripts).await;
// OR
let date_filter = test
.get_filter(FilterType::Data, Some(FieldType::DateTime))
.await
.unwrap();
let checkbox_filter_bytes: Result<Bytes, ProtobufError> = create_checkbox_filter().try_into();
let checkbox_filter_bytes = checkbox_filter_bytes.unwrap().to_vec();
let date_filter_bytes: Result<Bytes, ProtobufError> = create_date_filter().try_into();
let date_filter_bytes = date_filter_bytes.unwrap().to_vec();
let number_filter_bytes: Result<Bytes, ProtobufError> = create_number_filter().try_into();
let number_filter_bytes = number_filter_bytes.unwrap().to_vec();
let scripts = vec![
CreateDataFilter {
parent_filter_id: Some(date_filter.id),
field_type: FieldType::Number,
data: BoxAny::new(create_number_filter()),
changed: None,
},
Wait { millisecond: 100 },
AssertFilters {
expected: vec![FilterPB {
id: "".to_string(),
filter_type: FilterType::Or,
children: vec![
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::Checkbox,
data: checkbox_filter_bytes,
}),
},
FilterPB {
id: "".to_string(),
filter_type: FilterType::And,
children: vec![
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::DateTime,
data: date_filter_bytes,
}),
},
FilterPB {
id: "".to_string(),
filter_type: FilterType::Data,
children: vec![],
data: Some(FilterDataPB {
field_id: "".to_string(),
field_type: FieldType::Number,
data: number_filter_bytes,
}),
},
],
data: None,
},
],
data: None,
}],
},
AssertNumberOfVisibleRows { expected: 4 },
];
test.run_scripts(scripts).await;
// IS_CHECK OR (DATE > 1651366800 AND NUMBER NOT EMPTY)
}

View File

@ -1,4 +1,5 @@
use flowy_database2::entities::CheckboxFilterConditionPB;
use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, FieldType};
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
@ -6,27 +7,39 @@ use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}
#[tokio::test]
async fn grid_filter_checkbox_is_check_test() {
let mut test = DatabaseFilterTest::new().await;
let expected = 3;
let row_count = test.row_details.len();
// The initial number of unchecked is 3
// The initial number of checked is 2
let scripts = vec![CreateCheckboxFilter {
condition: CheckboxFilterConditionPB::IsChecked,
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - 3,
}),
}];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_checkbox_is_uncheck_test() {
let mut test = DatabaseFilterTest::new().await;
let expected = 4;
let row_count = test.row_details.len();
// The initial number of checked is 3
// The initial number of unchecked is 4
let scripts = vec![
CreateCheckboxFilter {
condition: CheckboxFilterConditionPB::IsUnChecked,
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Checkbox,
data: BoxAny::new(CheckboxFilterPB {
condition: CheckboxFilterConditionPB::IsChecked,
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
}),
},
AssertNumberOfVisibleRows { expected },
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_checkbox_is_uncheck_test() {
let mut test = DatabaseFilterTest::new().await;
let expected = 4;
let row_count = test.row_details.len();
let scripts = vec![
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Checkbox,
data: BoxAny::new(CheckboxFilterPB {
condition: CheckboxFilterConditionPB::IsUnChecked,
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,

View File

@ -1,5 +1,6 @@
use flowy_database2::entities::{ChecklistFilterConditionPB, FieldType};
use flowy_database2::entities::{ChecklistFilterConditionPB, ChecklistFilterPB, FieldType};
use flowy_database2::services::field::checklist_type_option::ChecklistCellData;
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
@ -16,8 +17,12 @@ async fn grid_filter_checklist_is_incomplete_test() {
row_id: test.row_details[0].row.id.clone(),
selected_option_ids: option_ids,
},
CreateChecklistFilter {
condition: ChecklistFilterConditionPB::IsIncomplete,
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Checklist,
data: BoxAny::new(ChecklistFilterPB {
condition: ChecklistFilterConditionPB::IsIncomplete,
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -39,8 +44,12 @@ async fn grid_filter_checklist_is_complete_test() {
row_id: test.row_details[0].row.id.clone(),
selected_option_ids: option_ids,
},
CreateChecklistFilter {
condition: ChecklistFilterConditionPB::IsComplete,
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Checklist,
data: BoxAny::new(ChecklistFilterPB {
condition: ChecklistFilterConditionPB::IsComplete,
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,

View File

@ -1,4 +1,5 @@
use flowy_database2::entities::DateFilterConditionPB;
use flowy_database2::entities::{DateFilterConditionPB, DateFilterPB, FieldType};
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
@ -9,11 +10,15 @@ async fn grid_filter_date_is_test() {
let row_count = test.row_details.len();
let expected = 3;
let scripts = vec![
CreateDateFilter {
condition: DateFilterConditionPB::DateIs,
start: None,
end: None,
timestamp: Some(1647251762),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::DateTime,
data: BoxAny::new(DateFilterPB {
condition: DateFilterConditionPB::DateIs,
start: None,
end: None,
timestamp: Some(1647251762),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -30,11 +35,15 @@ async fn grid_filter_date_after_test() {
let row_count = test.row_details.len();
let expected = 3;
let scripts = vec![
CreateDateFilter {
condition: DateFilterConditionPB::DateAfter,
start: None,
end: None,
timestamp: Some(1647251762),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::DateTime,
data: BoxAny::new(DateFilterPB {
condition: DateFilterConditionPB::DateAfter,
start: None,
end: None,
timestamp: Some(1647251762),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -51,11 +60,15 @@ async fn grid_filter_date_on_or_after_test() {
let row_count = test.row_details.len();
let expected = 3;
let scripts = vec![
CreateDateFilter {
condition: DateFilterConditionPB::DateOnOrAfter,
start: None,
end: None,
timestamp: Some(1668359085),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::DateTime,
data: BoxAny::new(DateFilterPB {
condition: DateFilterConditionPB::DateOnOrAfter,
start: None,
end: None,
timestamp: Some(1668359085),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -72,11 +85,15 @@ async fn grid_filter_date_on_or_before_test() {
let row_count = test.row_details.len();
let expected = 4;
let scripts = vec![
CreateDateFilter {
condition: DateFilterConditionPB::DateOnOrBefore,
start: None,
end: None,
timestamp: Some(1668359085),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::DateTime,
data: BoxAny::new(DateFilterPB {
condition: DateFilterConditionPB::DateOnOrBefore,
start: None,
end: None,
timestamp: Some(1668359085),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -93,11 +110,15 @@ async fn grid_filter_date_within_test() {
let row_count = test.row_details.len();
let expected = 5;
let scripts = vec![
CreateDateFilter {
condition: DateFilterConditionPB::DateWithIn,
start: Some(1647251762),
end: Some(1668704685),
timestamp: None,
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::DateTime,
data: BoxAny::new(DateFilterPB {
condition: DateFilterConditionPB::DateWithIn,
start: Some(1647251762),
end: Some(1668704685),
timestamp: None,
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,

View File

@ -1,3 +1,4 @@
mod advanced_filter_test;
mod checkbox_filter_test;
mod checklist_filter_test;
mod date_filter_test;

View File

@ -1,4 +1,5 @@
use flowy_database2::entities::NumberFilterConditionPB;
use flowy_database2::entities::{FieldType, NumberFilterConditionPB, NumberFilterPB};
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
@ -9,9 +10,13 @@ async fn grid_filter_number_is_equal_test() {
let row_count = test.row_details.len();
let expected = 1;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::Equal,
content: "1".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Number,
data: BoxAny::new(NumberFilterPB {
condition: NumberFilterConditionPB::Equal,
content: "1".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -28,9 +33,13 @@ async fn grid_filter_number_is_less_than_test() {
let row_count = test.row_details.len();
let expected = 2;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::LessThan,
content: "3".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Number,
data: BoxAny::new(NumberFilterPB {
condition: NumberFilterConditionPB::LessThan,
content: "3".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -48,9 +57,13 @@ async fn grid_filter_number_is_less_than_test2() {
let row_count = test.row_details.len();
let expected = 2;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::LessThan,
content: "$3".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Number,
data: BoxAny::new(NumberFilterPB {
condition: NumberFilterConditionPB::LessThan,
content: "$3".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -67,9 +80,13 @@ async fn grid_filter_number_is_less_than_or_equal_test() {
let row_count = test.row_details.len();
let expected = 3;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::LessThanOrEqualTo,
content: "3".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Number,
data: BoxAny::new(NumberFilterPB {
condition: NumberFilterConditionPB::LessThanOrEqualTo,
content: "3".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -86,9 +103,13 @@ async fn grid_filter_number_is_empty_test() {
let row_count = test.row_details.len();
let expected = 2;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::NumberIsEmpty,
content: "".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Number,
data: BoxAny::new(NumberFilterPB {
condition: NumberFilterConditionPB::NumberIsEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -105,9 +126,13 @@ async fn grid_filter_number_is_not_empty_test() {
let row_count = test.row_details.len();
let expected = 5;
let scripts = vec![
CreateNumberFilter {
condition: NumberFilterConditionPB::NumberIsNotEmpty,
content: "".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Number,
data: BoxAny::new(NumberFilterPB {
condition: NumberFilterConditionPB::NumberIsNotEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,

View File

@ -3,14 +3,12 @@
use std::time::Duration;
use collab_database::rows::RowId;
use flowy_database2::services::filter::FilterContext;
use flowy_database2::services::filter::{FilterChangeset, FilterInner};
use lib_infra::box_any::BoxAny;
use tokio::sync::broadcast::Receiver;
use flowy_database2::entities::{
CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB,
DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterPayloadPB, FieldType,
FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB,
TextFilterConditionPB, TextFilterPB, UpdateFilterParams, UpdateFilterPayloadPB,
DatabaseViewSettingPB, FieldType, FilterPB, FilterType, TextFilterConditionPB, TextFilterPB,
};
use flowy_database2::services::database_view::DatabaseViewChanged;
use lib_dispatch::prelude::af_spawn;
@ -37,12 +35,10 @@ pub enum FilterScript {
option_id: String,
changed: Option<FilterRowChanged>,
},
InsertFilter {
payload: UpdateFilterPayloadPB,
},
CreateTextFilter {
condition: TextFilterConditionPB,
content: String,
CreateDataFilter {
parent_filter_id: Option<String>,
field_type: FieldType,
data: BoxAny,
changed: Option<FilterRowChanged>,
},
UpdateTextFilter {
@ -51,50 +47,36 @@ pub enum FilterScript {
content: String,
changed: Option<FilterRowChanged>,
},
CreateNumberFilter {
condition: NumberFilterConditionPB,
content: String,
CreateAndFilter {
parent_filter_id: Option<String>,
changed: Option<FilterRowChanged>,
},
CreateCheckboxFilter {
condition: CheckboxFilterConditionPB,
CreateOrFilter {
parent_filter_id: Option<String>,
changed: Option<FilterRowChanged>,
},
CreateDateFilter {
condition: DateFilterConditionPB,
start: Option<i64>,
end: Option<i64>,
timestamp: Option<i64>,
changed: Option<FilterRowChanged>,
},
CreateMultiSelectFilter {
condition: SelectOptionConditionPB,
option_ids: Vec<String>,
},
CreateSingleSelectFilter {
condition: SelectOptionConditionPB,
option_ids: Vec<String>,
changed: Option<FilterRowChanged>,
},
CreateChecklistFilter {
condition: ChecklistFilterConditionPB,
changed: Option<FilterRowChanged>,
},
AssertFilterCount {
count: i32,
},
DeleteFilter {
filter_context: FilterContext,
filter_id: String,
field_id: String,
changed: Option<FilterRowChanged>,
},
AssertFilterContent {
filter_id: String,
condition: i64,
content: String,
// CreateSimpleAdvancedFilter,
// CreateComplexAdvancedFilter,
AssertFilterCount {
count: usize,
},
AssertNumberOfVisibleRows {
expected: usize,
},
AssertFilters {
/// 1. assert that the filter type is correct
/// 2. if the filter is data, assert that the field_type, condition and content are correct
/// (no field_id)
/// 3. if the filter is and/or, assert that each child is correct as well.
expected: Vec<FilterPB>,
},
// AssertSimpleAdvancedFilter,
// AssertComplexAdvancedFilterResult,
#[allow(dead_code)]
AssertGridSetting {
expected_setting: DatabaseViewSettingPB,
@ -118,14 +100,54 @@ impl DatabaseFilterTest {
}
}
pub fn view_id(&self) -> String {
self.view_id.clone()
}
pub async fn get_all_filters(&self) -> Vec<FilterPB> {
self.editor.get_all_filters(&self.view_id).await.items
}
pub async fn get_filter(
&self,
filter_type: FilterType,
field_type: Option<FieldType>,
) -> Option<FilterPB> {
let filters = self.inner.editor.get_all_filters(&self.view_id).await;
for filter in filters.items.iter() {
let result = Self::find_filter(filter, filter_type, field_type);
if result.is_some() {
return result;
}
}
None
}
fn find_filter(
filter: &FilterPB,
filter_type: FilterType,
field_type: Option<FieldType>,
) -> Option<FilterPB> {
match &filter.filter_type {
FilterType::And | FilterType::Or if filter.filter_type == filter_type => Some(filter.clone()),
FilterType::And | FilterType::Or => {
for child_filter in filter.children.iter() {
if let Some(result) = Self::find_filter(child_filter, filter_type, field_type) {
return Some(result);
}
}
None
},
FilterType::Data
if filter.filter_type == filter_type
&& field_type.map_or(false, |field_type| {
field_type == filter.data.clone().unwrap().field_type
}) =>
{
Some(filter.clone())
},
_ => None,
}
}
pub async fn run_scripts(&mut self, scripts: Vec<FilterScript>) {
for script in scripts {
self.run_script(script).await;
@ -139,13 +161,7 @@ impl DatabaseFilterTest {
text,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.subscribe_view_changed().await;
self.assert_future_changed(changed).await;
self.update_text_cell(row_id, &text).await.unwrap();
},
@ -163,46 +179,35 @@ impl DatabaseFilterTest {
option_id,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.subscribe_view_changed().await;
self.assert_future_changed(changed).await;
self
.update_single_select_cell(row_id, &option_id)
.await
.unwrap();
},
FilterScript::InsertFilter { payload } => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.insert_filter(payload).await;
},
FilterScript::CreateTextFilter {
condition,
content,
FilterScript::CreateDataFilter {
parent_filter_id,
field_type,
data,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.subscribe_view_changed().await;
self.assert_future_changed(changed).await;
let field = self.get_first_field(FieldType::RichText);
let text_filter = TextFilterPB { condition, content };
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, text_filter);
self.insert_filter(payload).await;
let field = self.get_first_field(field_type);
let params = FilterChangeset::Insert {
parent_filter_id,
data: FilterInner::Data {
field_id: field.id,
field_type,
condition_and_content: data,
},
};
self
.editor
.modify_view_filters(&self.view_id, params)
.await
.unwrap();
},
FilterScript::UpdateTextFilter {
filter,
@ -210,172 +215,76 @@ impl DatabaseFilterTest {
content,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.assert_future_changed(changed).await;
let params = UpdateFilterParams {
view_id: self.view_id(),
field_id: filter.field_id,
filter_id: Some(filter.id),
field_type: filter.field_type,
condition: condition as i64,
content,
};
self.editor.create_or_update_filter(params).await.unwrap();
},
FilterScript::CreateNumberFilter {
condition,
content,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.assert_future_changed(changed).await;
let field = self.get_first_field(FieldType::Number);
let number_filter = NumberFilterPB { condition, content };
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, number_filter);
self.insert_filter(payload).await;
},
FilterScript::CreateCheckboxFilter { condition, changed } => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.assert_future_changed(changed).await;
let field = self.get_first_field(FieldType::Checkbox);
let checkbox_filter = CheckboxFilterPB { condition };
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, checkbox_filter);
self.insert_filter(payload).await;
},
FilterScript::CreateDateFilter {
condition,
start,
end,
timestamp,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.assert_future_changed(changed).await;
let field = self.get_first_field(FieldType::DateTime);
let date_filter = DateFilterPB {
condition,
start,
end,
timestamp,
};
self.subscribe_view_changed().await;
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, date_filter);
self.insert_filter(payload).await;
},
FilterScript::CreateMultiSelectFilter {
condition,
option_ids,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
let field = self.get_first_field(FieldType::MultiSelect);
let filter = SelectOptionFilterPB {
condition,
option_ids,
self.assert_future_changed(changed).await;
let current_filter = filter.data.unwrap();
let params = FilterChangeset::UpdateData {
filter_id: filter.id,
data: FilterInner::Data {
field_id: current_filter.field_id,
field_type: current_filter.field_type,
condition_and_content: BoxAny::new(TextFilterPB { condition, content }),
},
};
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter);
self.insert_filter(payload).await;
self
.editor
.modify_view_filters(&self.view_id, params)
.await
.unwrap();
},
FilterScript::CreateSingleSelectFilter {
condition,
option_ids,
FilterScript::CreateAndFilter {
parent_filter_id,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.subscribe_view_changed().await;
self.assert_future_changed(changed).await;
let field = self.get_first_field(FieldType::SingleSelect);
let filter = SelectOptionFilterPB {
condition,
option_ids,
let params = FilterChangeset::Insert {
parent_filter_id,
data: FilterInner::And { children: vec![] },
};
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter);
self.insert_filter(payload).await;
self
.editor
.modify_view_filters(&self.view_id, params)
.await
.unwrap();
},
FilterScript::CreateChecklistFilter { condition, changed } => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
FilterScript::CreateOrFilter {
parent_filter_id,
changed,
} => {
self.subscribe_view_changed().await;
self.assert_future_changed(changed).await;
let field = self.get_first_field(FieldType::Checklist);
let filter = ChecklistFilterPB { condition };
let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter);
self.insert_filter(payload).await;
let params = FilterChangeset::Insert {
parent_filter_id,
data: FilterInner::Or { children: vec![] },
};
self
.editor
.modify_view_filters(&self.view_id, params)
.await
.unwrap();
},
FilterScript::AssertFilterCount { count } => {
let filters = self.editor.get_all_filters(&self.view_id).await.items;
assert_eq!(count as usize, filters.len());
},
FilterScript::AssertFilterContent {
filter_id,
condition,
content,
} => {
let filter = self
.editor
.get_filter(&self.view_id, &filter_id)
.await
.unwrap();
assert_eq!(&filter.content, &content);
assert_eq!(filter.condition, condition);
assert_eq!(count, filters.len());
},
FilterScript::DeleteFilter {
filter_context,
filter_id,
field_id,
changed,
} => {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id())
.await
.unwrap(),
);
self.subscribe_view_changed().await;
self.assert_future_changed(changed).await;
let params = DeleteFilterPayloadPB {
filter_id: filter_context.filter_id,
view_id: self.view_id(),
field_id: filter_context.field_id,
field_type: filter_context.field_type,
let params = FilterChangeset::Delete {
filter_id,
field_id,
};
self.editor.delete_filter(params).await.unwrap();
self
.editor
.modify_view_filters(&self.view_id, params)
.await
.unwrap();
},
FilterScript::AssertGridSetting { expected_setting } => {
let setting = self
@ -385,6 +294,12 @@ impl DatabaseFilterTest {
.unwrap();
assert_eq!(expected_setting, setting);
},
FilterScript::AssertFilters { expected } => {
let actual = self.get_all_filters().await;
for (actual_filter, expected_filter) in actual.iter().zip(expected.iter()) {
Self::assert_filter(actual_filter, expected_filter);
}
},
FilterScript::AssertNumberOfVisibleRows { expected } => {
let grid = self.editor.get_database_data(&self.view_id).await.unwrap();
assert_eq!(grid.rows.len(), expected);
@ -395,6 +310,16 @@ impl DatabaseFilterTest {
}
}
async fn subscribe_view_changed(&mut self) {
self.recv = Some(
self
.editor
.subscribe_view_changed(&self.view_id)
.await
.unwrap(),
);
}
async fn assert_future_changed(&mut self, change: Option<FilterRowChanged>) {
if change.is_none() {
return;
@ -424,9 +349,24 @@ impl DatabaseFilterTest {
});
}
async fn insert_filter(&self, payload: UpdateFilterPayloadPB) {
let params: UpdateFilterParams = payload.try_into().unwrap();
self.editor.create_or_update_filter(params).await.unwrap();
fn assert_filter(actual: &FilterPB, expected: &FilterPB) {
assert_eq!(actual.filter_type, expected.filter_type);
assert_eq!(actual.children.is_empty(), expected.children.is_empty());
assert_eq!(actual.data.is_some(), expected.data.is_some());
match actual.filter_type {
FilterType::Data => {
let actual_data = actual.data.clone().unwrap();
let expected_data = expected.data.clone().unwrap();
assert_eq!(actual_data.field_type, expected_data.field_type);
assert_eq!(actual_data.data, expected_data.data);
},
FilterType::And | FilterType::Or => {
for (actual_child, expected_child) in actual.children.iter().zip(expected.children.iter()) {
Self::assert_filter(actual_child, expected_child);
}
},
}
}
}

View File

@ -1,4 +1,5 @@
use flowy_database2::entities::{FieldType, SelectOptionConditionPB};
use flowy_database2::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB};
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
@ -7,9 +8,14 @@ use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}
async fn grid_filter_multi_select_is_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![
CreateMultiSelectFilter {
condition: SelectOptionConditionPB::OptionIsEmpty,
option_ids: vec![],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::MultiSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsEmpty,
option_ids: vec![],
}),
changed: None,
},
AssertNumberOfVisibleRows { expected: 2 },
];
@ -20,9 +26,14 @@ async fn grid_filter_multi_select_is_empty_test() {
async fn grid_filter_multi_select_is_not_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![
CreateMultiSelectFilter {
condition: SelectOptionConditionPB::OptionIsNotEmpty,
option_ids: vec![],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::MultiSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsNotEmpty,
option_ids: vec![],
}),
changed: None,
},
AssertNumberOfVisibleRows { expected: 5 },
];
@ -35,9 +46,14 @@ async fn grid_filter_multi_select_is_test() {
let field = test.get_first_field(FieldType::MultiSelect);
let mut options = test.get_multi_select_type_option(&field.id);
let scripts = vec![
CreateMultiSelectFilter {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(0).id, options.remove(0).id],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::MultiSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(0).id, options.remove(0).id],
}),
changed: None,
},
AssertNumberOfVisibleRows { expected: 5 },
];
@ -50,9 +66,14 @@ async fn grid_filter_multi_select_is_test2() {
let field = test.get_first_field(FieldType::MultiSelect);
let mut options = test.get_multi_select_type_option(&field.id);
let scripts = vec![
CreateMultiSelectFilter {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(1).id],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::MultiSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(1).id],
}),
changed: None,
},
AssertNumberOfVisibleRows { expected: 4 },
];
@ -65,9 +86,13 @@ async fn grid_filter_single_select_is_empty_test() {
let expected = 3;
let row_count = test.row_details.len();
let scripts = vec![
CreateSingleSelectFilter {
condition: SelectOptionConditionPB::OptionIsEmpty,
option_ids: vec![],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::SingleSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIsEmpty,
option_ids: vec![],
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -86,9 +111,13 @@ async fn grid_filter_single_select_is_test() {
let expected = 2;
let row_count = test.row_details.len();
let scripts = vec![
CreateSingleSelectFilter {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(0).id],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::SingleSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![options.remove(0).id],
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
@ -109,9 +138,13 @@ async fn grid_filter_single_select_is_test2() {
let row_count = test.row_details.len();
let scripts = vec![
CreateSingleSelectFilter {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![option.id.clone()],
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::SingleSelect,
data: BoxAny::new(SelectOptionFilterPB {
condition: SelectOptionConditionPB::OptionIs,
option_ids: vec![option.id.clone()],
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - 2,

View File

@ -1,7 +1,5 @@
use flowy_database2::entities::{
FieldType, TextFilterConditionPB, TextFilterPB, UpdateFilterPayloadPB,
};
use flowy_database2::services::filter::FilterContext;
use flowy_database2::entities::{FieldType, TextFilterConditionPB, TextFilterPB};
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::*;
@ -10,9 +8,13 @@ use crate::database::filter_test::script::*;
async fn grid_filter_text_is_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![
CreateTextFilter {
condition: TextFilterConditionPB::TextIsEmpty,
content: "".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::TextIsEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 5,
@ -28,9 +30,13 @@ async fn grid_filter_text_is_not_empty_test() {
let mut test = DatabaseFilterTest::new().await;
// Only one row's text of the initial rows is ""
let scripts = vec![
CreateTextFilter {
condition: TextFilterConditionPB::TextIsNotEmpty,
content: "".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::TextIsNotEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 1,
@ -44,7 +50,8 @@ async fn grid_filter_text_is_not_empty_test() {
test
.run_scripts(vec![
DeleteFilter {
filter_context: FilterContext::from(&filter),
filter_id: filter.id,
field_id: filter.data.unwrap().field_id,
changed: Some(FilterRowChanged {
showing_num_of_rows: 1,
hiding_num_of_rows: 0,
@ -59,9 +66,13 @@ async fn grid_filter_text_is_not_empty_test() {
async fn grid_filter_is_text_test() {
let mut test = DatabaseFilterTest::new().await;
// Only one row's text of the initial rows is "A"
let scripts = vec![CreateTextFilter {
condition: TextFilterConditionPB::Is,
content: "A".to_string(),
let scripts = vec![CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::Is,
content: "A".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 5,
@ -73,9 +84,13 @@ async fn grid_filter_is_text_test() {
#[tokio::test]
async fn grid_filter_contain_text_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![CreateTextFilter {
condition: TextFilterConditionPB::Contains,
content: "A".to_string(),
let scripts = vec![CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::Contains,
content: "A".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 2,
@ -90,9 +105,13 @@ async fn grid_filter_contain_text_test2() {
let row_detail = test.row_details.clone();
let scripts = vec![
CreateTextFilter {
condition: TextFilterConditionPB::Contains,
content: "A".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::Contains,
content: "A".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 2,
@ -114,9 +133,13 @@ async fn grid_filter_contain_text_test2() {
async fn grid_filter_does_not_contain_text_test() {
let mut test = DatabaseFilterTest::new().await;
// None of the initial rows contains the text "AB"
let scripts = vec![CreateTextFilter {
condition: TextFilterConditionPB::DoesNotContain,
content: "AB".to_string(),
let scripts = vec![CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::DoesNotContain,
content: "AB".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 0,
@ -128,9 +151,13 @@ async fn grid_filter_does_not_contain_text_test() {
#[tokio::test]
async fn grid_filter_start_with_text_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![CreateTextFilter {
condition: TextFilterConditionPB::StartsWith,
content: "A".to_string(),
let scripts = vec![CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::StartsWith,
content: "A".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 3,
@ -143,9 +170,13 @@ async fn grid_filter_start_with_text_test() {
async fn grid_filter_ends_with_text_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![
CreateTextFilter {
condition: TextFilterConditionPB::EndsWith,
content: "A".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::EndsWith,
content: "A".to_string(),
}),
changed: None,
},
AssertNumberOfVisibleRows { expected: 2 },
@ -157,9 +188,13 @@ async fn grid_filter_ends_with_text_test() {
async fn grid_update_text_filter_test() {
let mut test = DatabaseFilterTest::new().await;
let scripts = vec![
CreateTextFilter {
condition: TextFilterConditionPB::EndsWith,
content: "A".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::EndsWith,
content: "A".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 4,
@ -190,14 +225,16 @@ async fn grid_update_text_filter_test() {
#[tokio::test]
async fn grid_filter_delete_test() {
let mut test = DatabaseFilterTest::new().await;
let field = test.get_first_field(FieldType::RichText).clone();
let text_filter = TextFilterPB {
condition: TextFilterConditionPB::TextIsEmpty,
content: "".to_string(),
};
let payload = UpdateFilterPayloadPB::new(&test.view_id(), &field, text_filter);
let scripts = vec![
InsertFilter { payload },
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
changed: None,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::TextIsEmpty,
content: "".to_string(),
}),
},
AssertFilterCount { count: 1 },
AssertNumberOfVisibleRows { expected: 1 },
];
@ -207,7 +244,8 @@ async fn grid_filter_delete_test() {
test
.run_scripts(vec![
DeleteFilter {
filter_context: FilterContext::from(&filter),
filter_id: filter.id,
field_id: filter.data.unwrap().field_id,
changed: None,
},
AssertFilterCount { count: 0 },
@ -221,9 +259,13 @@ async fn grid_filter_update_empty_text_cell_test() {
let mut test = DatabaseFilterTest::new().await;
let row_details = test.row_details.clone();
let scripts = vec![
CreateTextFilter {
condition: TextFilterConditionPB::TextIsEmpty,
content: "".to_string(),
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::RichText,
data: BoxAny::new(TextFilterPB {
condition: TextFilterConditionPB::TextIsEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: 5,

View File

@ -2,6 +2,7 @@ use std::any::Any;
use anyhow::Result;
#[derive(Debug)]
pub struct BoxAny(Box<dyn Any + Send + Sync + 'static>);
impl BoxAny {
@ -12,6 +13,13 @@ impl BoxAny {
Self(Box::new(value))
}
pub fn cloned<T>(&self) -> Option<T>
where
T: Clone + 'static,
{
self.0.downcast_ref::<T>().cloned()
}
pub fn unbox_or_default<T>(self) -> T
where
T: Default + 'static,