chore: kanban filter mvp (#4935)

* chore: add filter controller in group controller

* chore: enable url filter

* chore: bump collab rev

* chore: fix test

* chore: bump collab rev
This commit is contained in:
Richard Shiue
2024-03-21 12:54:56 +08:00
committed by GitHub
parent ef9891abfe
commit 37f521ae57
40 changed files with 661 additions and 429 deletions

View File

@ -50,7 +50,7 @@ APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend" FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend" TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend" WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm" WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
# Test default config # Test default config
TEST_CRATE_TYPE = "cdylib" TEST_CRATE_TYPE = "cdylib"
TEST_LIB_EXT = "dylib" TEST_LIB_EXT = "dylib"
@ -226,9 +226,8 @@ script = ['''
echo FEATURES: ${FLUTTER_DESKTOP_FEATURES} echo FEATURES: ${FLUTTER_DESKTOP_FEATURES}
echo PRODUCT_EXT: ${PRODUCT_EXT} echo PRODUCT_EXT: ${PRODUCT_EXT}
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT} echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
echo ${platforms} echo BUILD_ARCHS: ${BUILD_ARCHS}
echo ${BUILD_ARCHS} echo BUILD_VERSION: ${BUILD_VERSION}
echo ${BUILD_VERSION}
'''] ''']
script_runner = "@shell" script_runner = "@shell"

View File

@ -47,7 +47,7 @@ class FieldInfo with _$FieldInfo {
} }
bool get canCreateFilter { bool get canCreateFilter {
if (hasFilter) { if (isGroupField) {
return false; return false;
} }
@ -58,6 +58,7 @@ class FieldInfo with _$FieldInfo {
case FieldType.RichText: case FieldType.RichText:
case FieldType.SingleSelect: case FieldType.SingleSelect:
case FieldType.Checklist: case FieldType.Checklist:
case FieldType.URL:
return true; return true;
default: default:
return false; return false;

View File

@ -385,16 +385,23 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
groupList.insert(insertGroups.index, group); groupList.insert(insertGroups.index, group);
add(BoardEvent.didReceiveGroups(groupList)); add(BoardEvent.didReceiveGroups(groupList));
}, },
onUpdateGroup: (updatedGroups) { onUpdateGroup: (updatedGroups) async {
if (isClosed) { if (isClosed) {
return; return;
} }
// workaround: update group most of the time gets called before fields in
// field controller are updated. For single and multi-select group
// renames, this is required before generating the new group name.
await Future.delayed(const Duration(milliseconds: 50));
for (final group in updatedGroups) { for (final group in updatedGroups) {
// see if the column is already in the board // see if the column is already in the board
final index = groupList.indexWhere((g) => g.groupId == group.groupId); final index = groupList.indexWhere((g) => g.groupId == group.groupId);
if (index == -1) continue; if (index == -1) {
continue;
}
final columnController = final columnController =
boardController.getGroupController(group.groupId); boardController.getGroupController(group.groupId);
if (columnController != null) { if (columnController != null) {

View File

@ -1,5 +1,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:flutter/material.dart' hide Card; import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -34,6 +36,8 @@ import 'toolbar/board_setting_bar.dart';
import 'widgets/board_hidden_groups.dart'; import 'widgets/board_hidden_groups.dart';
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override @override
Widget content( Widget content(
BuildContext context, BuildContext context,
@ -49,14 +53,27 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
BoardSettingBar( BoardSettingBar(
key: _makeValueKey(controller), key: _makeValueKey(controller),
databaseController: controller, databaseController: controller,
toggleExtension: _toggleExtension,
); );
@override @override
Widget settingBarExtension( Widget settingBarExtension(
BuildContext context, BuildContext context,
DatabaseController controller, DatabaseController controller,
) => ) {
const SizedBox.shrink(); return DatabaseViewSettingExtension(
key: _makeValueKey(controller),
viewId: controller.viewId,
databaseController: controller,
toggleExtension: _toggleExtension,
);
}
@override
void dispose() {
_toggleExtension.dispose();
super.dispose();
}
ValueKey _makeValueKey(DatabaseController controller) => ValueKey _makeValueKey(DatabaseController controller) =>
ValueKey(controller.viewId); ValueKey(controller.viewId);

View File

@ -1,24 +1,53 @@
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardSettingBar extends StatelessWidget { class BoardSettingBar extends StatelessWidget {
const BoardSettingBar({ const BoardSettingBar({
super.key, super.key,
required this.databaseController, required this.databaseController,
required this.toggleExtension,
}); });
final DatabaseController databaseController; final DatabaseController databaseController;
final ToggleExtensionNotifier toggleExtension;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return BlocProvider<DatabaseFilterMenuBloc>(
height: 20, create: (context) => DatabaseFilterMenuBloc(
child: Row( viewId: databaseController.viewId,
mainAxisAlignment: MainAxisAlignment.end, fieldController: databaseController.fieldController,
children: [ )..add(const DatabaseFilterMenuEvent.initial()),
SettingButton(databaseController: databaseController), child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
], listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
child: ValueListenableBuilder<bool>(
valueListenable: databaseController.isLoading,
builder: (context, value, child) {
if (value) {
return const SizedBox.shrink();
}
return SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const FilterButton(),
const HSpace(2),
SettingButton(
databaseController: databaseController,
),
],
),
);
},
),
), ),
); );
} }

View File

@ -1,15 +1,6 @@
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:fixnum/fixnum.dart' as $fixnum;
@ -109,25 +100,21 @@ class FilterBackendService {
int? timestamp, int? timestamp,
}) { }) {
assert( assert(
[ fieldType == FieldType.DateTime ||
FieldType.DateTime, fieldType == FieldType.LastEditedTime ||
FieldType.LastEditedTime, fieldType == FieldType.CreatedTime,
FieldType.CreatedTime,
].contains(fieldType),
); );
final filter = DateFilterPB(); final filter = DateFilterPB();
if (timestamp != null) { if (timestamp != null) {
filter.timestamp = $fixnum.Int64(timestamp); filter.timestamp = $fixnum.Int64(timestamp);
} else { }
if (start != null && end != null) { if (start != null) {
filter.start = $fixnum.Int64(start); filter.start = $fixnum.Int64(start);
filter.end = $fixnum.Int64(end); }
} else { if (end != null) {
throw Exception( filter.end = $fixnum.Int64(end);
"Start and end should not be null if the timestamp is null",
);
}
} }
return filterId == null return filterId == null

View File

@ -8,11 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'filter_menu_bloc.freezed.dart'; part 'filter_menu_bloc.freezed.dart';
class GridFilterMenuBloc class DatabaseFilterMenuBloc
extends Bloc<GridFilterMenuEvent, GridFilterMenuState> { extends Bloc<DatabaseFilterMenuEvent, DatabaseFilterMenuState> {
GridFilterMenuBloc({required this.viewId, required this.fieldController}) DatabaseFilterMenuBloc({required this.viewId, required this.fieldController})
: super( : super(
GridFilterMenuState.initial( DatabaseFilterMenuState.initial(
viewId, viewId,
fieldController.filterInfos, fieldController.filterInfos,
fieldController.fieldInfos, fieldController.fieldInfos,
@ -27,7 +27,7 @@ class GridFilterMenuBloc
void Function(List<FieldInfo>)? _onFieldFn; void Function(List<FieldInfo>)? _onFieldFn;
void _dispatch() { void _dispatch() {
on<GridFilterMenuEvent>( on<DatabaseFilterMenuEvent>(
(event, emit) async { (event, emit) async {
event.when( event.when(
initial: () { initial: () {
@ -55,11 +55,11 @@ class GridFilterMenuBloc
void _startListening() { void _startListening() {
_onFilterFn = (filters) { _onFilterFn = (filters) {
add(GridFilterMenuEvent.didReceiveFilters(filters)); add(DatabaseFilterMenuEvent.didReceiveFilters(filters));
}; };
_onFieldFn = (fields) { _onFieldFn = (fields) {
add(GridFilterMenuEvent.didReceiveFields(fields)); add(DatabaseFilterMenuEvent.didReceiveFields(fields));
}; };
fieldController.addListener( fieldController.addListener(
@ -87,32 +87,33 @@ class GridFilterMenuBloc
} }
@freezed @freezed
class GridFilterMenuEvent with _$GridFilterMenuEvent { class DatabaseFilterMenuEvent with _$DatabaseFilterMenuEvent {
const factory GridFilterMenuEvent.initial() = _Initial; const factory DatabaseFilterMenuEvent.initial() = _Initial;
const factory GridFilterMenuEvent.didReceiveFilters( const factory DatabaseFilterMenuEvent.didReceiveFilters(
List<FilterInfo> filters, List<FilterInfo> filters,
) = _DidReceiveFilters; ) = _DidReceiveFilters;
const factory GridFilterMenuEvent.didReceiveFields(List<FieldInfo> fields) = const factory DatabaseFilterMenuEvent.didReceiveFields(
_DidReceiveFields; List<FieldInfo> fields,
const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility; ) = _DidReceiveFields;
const factory DatabaseFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
} }
@freezed @freezed
class GridFilterMenuState with _$GridFilterMenuState { class DatabaseFilterMenuState with _$DatabaseFilterMenuState {
const factory GridFilterMenuState({ const factory DatabaseFilterMenuState({
required String viewId, required String viewId,
required List<FilterInfo> filters, required List<FilterInfo> filters,
required List<FieldInfo> fields, required List<FieldInfo> fields,
required List<FieldInfo> creatableFields, required List<FieldInfo> creatableFields,
required bool isVisible, required bool isVisible,
}) = _GridFilterMenuState; }) = _DatabaseFilterMenuState;
factory GridFilterMenuState.initial( factory DatabaseFilterMenuState.initial(
String viewId, String viewId,
List<FilterInfo> filterInfos, List<FilterInfo> filterInfos,
List<FieldInfo> fields, List<FieldInfo> fields,
) => ) =>
GridFilterMenuState( DatabaseFilterMenuState(
viewId: viewId, viewId: viewId,
filters: filterInfos, filters: filterInfos,
fields: fields, fields: fields,

View File

@ -3,8 +3,7 @@ import 'dart:async';
import 'package:appflowy/plugins/database/domain/filter_listener.dart'; import 'package:appflowy/plugins/database/domain/filter_listener.dart';
import 'package:appflowy/plugins/database/domain/filter_service.dart'; import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -12,7 +11,7 @@ part 'text_filter_editor_bloc.freezed.dart';
class TextFilterEditorBloc class TextFilterEditorBloc
extends Bloc<TextFilterEditorEvent, TextFilterEditorState> { extends Bloc<TextFilterEditorEvent, TextFilterEditorState> {
TextFilterEditorBloc({required this.filterInfo}) TextFilterEditorBloc({required this.filterInfo, required this.fieldType})
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
_listener = FilterListener( _listener = FilterListener(
viewId: filterInfo.viewId, viewId: filterInfo.viewId,
@ -23,6 +22,7 @@ class TextFilterEditorBloc
} }
final FilterInfo filterInfo; final FilterInfo filterInfo;
final FieldType fieldType;
final FilterBackendService _filterBackendSvc; final FilterBackendService _filterBackendSvc;
final FilterListener _listener; final FilterListener _listener;
@ -34,20 +34,34 @@ class TextFilterEditorBloc
_startListening(); _startListening();
}, },
updateCondition: (TextFilterConditionPB condition) { updateCondition: (TextFilterConditionPB condition) {
_filterBackendSvc.insertTextFilter( fieldType == FieldType.RichText
filterId: filterInfo.filter.id, ? _filterBackendSvc.insertTextFilter(
fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id,
condition: condition, fieldId: filterInfo.fieldInfo.id,
content: state.filter.content, condition: condition,
); content: state.filter.content,
)
: _filterBackendSvc.insertURLFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: condition,
content: state.filter.content,
);
}, },
updateContent: (content) { updateContent: (String content) {
_filterBackendSvc.insertTextFilter( fieldType == FieldType.RichText
filterId: filterInfo.filter.id, ? _filterBackendSvc.insertTextFilter(
fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id,
condition: state.filter.condition, fieldId: filterInfo.fieldInfo.id,
content: content, condition: state.filter.condition,
); content: content,
)
: _filterBackendSvc.insertURLFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: state.filter.condition,
content: content,
);
}, },
delete: () { delete: () {
_filterBackendSvc.deleteFilter( _filterBackendSvc.deleteFilter(

View File

@ -1,59 +1,44 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/filter/text_filter_editor_bloc.dart';
import '../condition_button.dart'; import '../condition_button.dart';
import '../disclosure_button.dart'; import '../disclosure_button.dart';
import '../filter_info.dart'; import '../filter_info.dart';
import 'choicechip.dart'; import 'choicechip.dart';
class TextFilterChoicechip extends StatefulWidget { class TextFilterChoicechip extends StatelessWidget {
const TextFilterChoicechip({required this.filterInfo, super.key}); const TextFilterChoicechip({required this.filterInfo, super.key});
final FilterInfo filterInfo; final FilterInfo filterInfo;
@override
State<TextFilterChoicechip> createState() => _TextFilterChoicechipState();
}
class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
late TextFilterEditorBloc bloc;
@override
void initState() {
bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const TextFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider(
value: bloc, create: (_) => TextFilterEditorBloc(
filterInfo: filterInfo,
fieldType: FieldType.RichText,
)..add(const TextFilterEditorEvent.initial()),
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>( child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (blocContext, state) { builder: (context, state) {
return AppFlowyPopover( return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 76)), constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (BuildContext context) { popupBuilder: (popoverContext) {
return TextFilterEditor(bloc: bloc); return BlocProvider.value(
value: context.read<TextFilterEditorBloc>(),
child: const TextFilterEditor(),
);
}, },
child: ChoiceChipButton( child: ChoiceChipButton(
filterInfo: widget.filterInfo, filterInfo: filterInfo,
filterDesc: _makeFilterDesc(state), filterDesc: _makeFilterDesc(state),
), ),
); );
@ -78,9 +63,7 @@ class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
} }
class TextFilterEditor extends StatefulWidget { class TextFilterEditor extends StatefulWidget {
const TextFilterEditor({required this.bloc, super.key}); const TextFilterEditor({super.key});
final TextFilterEditorBloc bloc;
@override @override
State<TextFilterEditor> createState() => _TextFilterEditorState(); State<TextFilterEditor> createState() => _TextFilterEditorState();
@ -91,26 +74,23 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
value: widget.bloc, builder: (context, state) {
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>( final List<Widget> children = [
builder: (context, state) { _buildFilterPanel(context, state),
final List<Widget> children = [ ];
_buildFilterPanel(context, state),
];
if (state.filter.condition != TextFilterConditionPB.TextIsEmpty && if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) { state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
children.add(const VSpace(4)); children.add(const VSpace(4));
children.add(_buildFilterTextField(context, state)); children.add(_buildFilterTextField(context, state));
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)), child: IntrinsicHeight(child: Column(children: children)),
); );
}, },
),
); );
} }

View File

@ -1,14 +1,58 @@
import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../filter_info.dart'; import '../filter_info.dart';
import 'choicechip.dart'; import 'choicechip.dart';
class URLFilterChoicechip extends StatelessWidget { class URLFilterChoiceChip extends StatelessWidget {
const URLFilterChoicechip({required this.filterInfo, super.key}); const URLFilterChoiceChip({required this.filterInfo, super.key});
final FilterInfo filterInfo; final FilterInfo filterInfo;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo); return BlocProvider(
create: (_) => TextFilterEditorBloc(
filterInfo: filterInfo,
fieldType: FieldType.URL,
),
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
builder: (context, state) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (popoverContext) {
return BlocProvider.value(
value: context.read<TextFilterEditorBloc>(),
child: const TextFilterEditor(),
);
},
child: ChoiceChipButton(
filterInfo: filterInfo,
filterDesc: _makeFilterDesc(state),
),
);
},
),
);
}
String _makeFilterDesc(TextFilterEditorState state) {
String filterDesc = state.filter.condition.choicechipPrefix;
if (state.filter.condition == TextFilterConditionPB.TextIsEmpty ||
state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) {
return filterDesc;
}
if (state.filter.content.isNotEmpty) {
filterDesc += " ${state.filter.content}";
}
return filterDesc;
} }
} }

View File

@ -21,13 +21,17 @@ class FilterInfo {
String get fieldId => filter.data.fieldId; String get fieldId => filter.data.fieldId;
DateFilterPB? dateFilter() { DateFilterPB? dateFilter() {
return filter.data.fieldType == FieldType.DateTime final fieldType = filter.data.fieldType;
return fieldType == FieldType.DateTime ||
fieldType == FieldType.CreatedTime ||
fieldType == FieldType.LastEditedTime
? DateFilterPB.fromBuffer(filter.data.data) ? DateFilterPB.fromBuffer(filter.data.data)
: null; : null;
} }
TextFilterPB? textFilter() { TextFilterPB? textFilter() {
return filter.data.fieldType == FieldType.RichText return filter.data.fieldType == FieldType.RichText ||
filter.data.fieldType == FieldType.URL
? TextFilterPB.fromBuffer(filter.data.data) ? TextFilterPB.fromBuffer(filter.data.data)
: null; : null;
} }

View File

@ -23,14 +23,14 @@ class FilterMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<GridFilterMenuBloc>( return BlocProvider<DatabaseFilterMenuBloc>(
create: (context) => GridFilterMenuBloc( create: (context) => DatabaseFilterMenuBloc(
viewId: fieldController.viewId, viewId: fieldController.viewId,
fieldController: fieldController, fieldController: fieldController,
)..add( )..add(
const GridFilterMenuEvent.initial(), const DatabaseFilterMenuEvent.initial(),
), ),
child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>( child: BlocBuilder<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
builder: (context, state) { builder: (context, state) {
final List<Widget> children = []; final List<Widget> children = [];
children.addAll( children.addAll(
@ -115,7 +115,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: child, child: child,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
final bloc = buildContext.read<GridFilterMenuBloc>(); final bloc = buildContext.read<DatabaseFilterMenuBloc>();
return GridCreateFilterList( return GridCreateFilterList(
viewId: widget.viewId, viewId: widget.viewId,
fieldController: bloc.fieldController, fieldController: bloc.fieldController,

View File

@ -26,7 +26,7 @@ class FilterMenuItem extends StatelessWidget {
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
FieldType.SingleSelect => FieldType.SingleSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo), SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.URL => URLFilterChoicechip(filterInfo: filterInfo), FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
_ => const SizedBox(), _ => const SizedBox(),
}; };

View File

@ -23,7 +23,7 @@ class _FilterButtonState extends State<FilterButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>( return BlocBuilder<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
builder: (context, state) { builder: (context, state) {
final textColor = state.filters.isEmpty final textColor = state.filters.isEmpty
? AFThemeExtension.of(context).textColor ? AFThemeExtension.of(context).textColor
@ -41,11 +41,11 @@ class _FilterButtonState extends State<FilterButton> {
padding: GridSize.toolbarSettingButtonInsets, padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border, radius: Corners.s4Border,
onPressed: () { onPressed: () {
final bloc = context.read<GridFilterMenuBloc>(); final bloc = context.read<DatabaseFilterMenuBloc>();
if (bloc.state.filters.isEmpty) { if (bloc.state.filters.isEmpty) {
_popoverController.show(); _popoverController.show();
} else { } else {
bloc.add(const GridFilterMenuEvent.toggleMenu()); bloc.add(const DatabaseFilterMenuEvent.toggleMenu());
} }
}, },
), ),
@ -63,14 +63,14 @@ class _FilterButtonState extends State<FilterButton> {
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: child, child: child,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
final bloc = buildContext.read<GridFilterMenuBloc>(); final bloc = buildContext.read<DatabaseFilterMenuBloc>();
return GridCreateFilterList( return GridCreateFilterList(
viewId: bloc.viewId, viewId: bloc.viewId,
fieldController: bloc.fieldController, fieldController: bloc.fieldController,
onClosed: () => _popoverController.close(), onClosed: () => _popoverController.close(),
onCreateFilter: () { onCreateFilter: () {
if (!bloc.state.isVisible) { if (!bloc.state.isVisible) {
bloc.add(const GridFilterMenuEvent.toggleMenu()); bloc.add(const DatabaseFilterMenuEvent.toggleMenu());
} }
}, },
); );

View File

@ -24,11 +24,11 @@ class GridSettingBar extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<GridFilterMenuBloc>( BlocProvider<DatabaseFilterMenuBloc>(
create: (context) => GridFilterMenuBloc( create: (context) => DatabaseFilterMenuBloc(
viewId: controller.viewId, viewId: controller.viewId,
fieldController: controller.fieldController, fieldController: controller.fieldController,
)..add(const GridFilterMenuEvent.initial()), )..add(const DatabaseFilterMenuEvent.initial()),
), ),
BlocProvider<SortEditorBloc>( BlocProvider<SortEditorBloc>(
create: (context) => SortEditorBloc( create: (context) => SortEditorBloc(
@ -37,7 +37,7 @@ class GridSettingBar extends StatelessWidget {
), ),
), ),
], ],
child: BlocListener<GridFilterMenuBloc, GridFilterMenuState>( child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible, listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(), listener: (context, state) => toggleExtension.toggle(),
child: ValueListenableBuilder<bool>( child: ValueListenableBuilder<bool>(

View File

@ -27,11 +27,11 @@ class MobileDatabaseControls extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<GridFilterMenuBloc>( BlocProvider<DatabaseFilterMenuBloc>(
create: (context) => GridFilterMenuBloc( create: (context) => DatabaseFilterMenuBloc(
viewId: controller.viewId, viewId: controller.viewId,
fieldController: controller.fieldController, fieldController: controller.fieldController,
)..add(const GridFilterMenuEvent.initial()), )..add(const DatabaseFilterMenuEvent.initial()),
), ),
BlocProvider<SortEditorBloc>( BlocProvider<SortEditorBloc>(
create: (context) => SortEditorBloc( create: (context) => SortEditorBloc(
@ -40,7 +40,7 @@ class MobileDatabaseControls extends StatelessWidget {
), ),
), ),
], ],
child: BlocListener<GridFilterMenuBloc, GridFilterMenuState>( child: BlocListener<DatabaseFilterMenuBloc, DatabaseFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible, listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(), listener: (context, state) => toggleExtension.toggle(),
child: ValueListenableBuilder<bool>( child: ValueListenableBuilder<bool>(

View File

@ -13,10 +13,10 @@ void main() {
test('test filter menu after create a text filter)', () async { test('test filter menu after create a text filter)', () async {
final context = await gridTest.createTestGrid(); final context = await gridTest.createTestGrid();
final menuBloc = GridFilterMenuBloc( final menuBloc = DatabaseFilterMenuBloc(
viewId: context.gridView.id, viewId: context.gridView.id,
fieldController: context.fieldController, fieldController: context.fieldController,
)..add(const GridFilterMenuEvent.initial()); )..add(const DatabaseFilterMenuEvent.initial());
await gridResponseFuture(); await gridResponseFuture();
assert(menuBloc.state.creatableFields.length == 3); assert(menuBloc.state.creatableFields.length == 3);
@ -28,15 +28,15 @@ void main() {
content: "", content: "",
); );
await gridResponseFuture(); await gridResponseFuture();
assert(menuBloc.state.creatableFields.length == 2); assert(menuBloc.state.creatableFields.length == 3);
}); });
test('test filter menu after update existing text filter)', () async { test('test filter menu after update existing text filter)', () async {
final context = await gridTest.createTestGrid(); final context = await gridTest.createTestGrid();
final menuBloc = GridFilterMenuBloc( final menuBloc = DatabaseFilterMenuBloc(
viewId: context.gridView.id, viewId: context.gridView.id,
fieldController: context.fieldController, fieldController: context.fieldController,
)..add(const GridFilterMenuEvent.initial()); )..add(const DatabaseFilterMenuEvent.initial());
await gridResponseFuture(); await gridResponseFuture();
final service = FilterBackendService(viewId: context.gridView.id); final service = FilterBackendService(viewId: context.gridView.id);

View File

@ -838,7 +838,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -862,7 +862,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -892,7 +892,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -911,7 +911,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -926,7 +926,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -963,7 +963,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1002,7 +1002,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",

View File

@ -96,10 +96,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }

View File

@ -65,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }

View File

@ -764,7 +764,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -788,7 +788,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -818,7 +818,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -837,7 +837,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-entity" name = "collab-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -852,7 +852,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -889,7 +889,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -928,7 +928,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4b25d7d021a11c51583e6a404c139f49ee6a3bf9#4b25d7d021a11c51583e6a404c139f49ee6a3bf9"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -1125,7 +1125,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf 0.8.0", "phf 0.11.2",
"smallvec", "smallvec",
] ]
@ -3663,7 +3663,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [ dependencies = [
"phf_macros", "phf_macros 0.8.0",
"phf_shared 0.8.0", "phf_shared 0.8.0",
"proc-macro-hack", "proc-macro-hack",
] ]
@ -3683,6 +3683,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [ dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2", "phf_shared 0.11.2",
] ]
@ -3750,6 +3751,19 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.47",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.8.0" version = "0.8.0"

View File

@ -120,10 +120,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4b25d7d021a11c51583e6a404c139f49ee6a3bf9" }

View File

@ -23,19 +23,19 @@ pub struct DateFilterPB {
} }
#[derive(Deserialize, Serialize, Default, Clone, Debug)] #[derive(Deserialize, Serialize, Default, Clone, Debug)]
pub struct DateFilterContentPB { pub struct DateFilterContent {
pub start: Option<i64>, pub start: Option<i64>,
pub end: Option<i64>, pub end: Option<i64>,
pub timestamp: Option<i64>, pub timestamp: Option<i64>,
} }
impl ToString for DateFilterContentPB { impl ToString for DateFilterContent {
fn to_string(&self) -> String { fn to_string(&self) -> String {
serde_json::to_string(self).unwrap() serde_json::to_string(self).unwrap()
} }
} }
impl FromStr for DateFilterContentPB { impl FromStr for DateFilterContent {
type Err = serde_json::Error; type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -89,7 +89,7 @@ impl ParseFilterData for DateFilterPB {
..Default::default() ..Default::default()
}; };
if let Ok(content) = DateFilterContentPB::from_str(&content) { if let Ok(content) = DateFilterContent::from_str(&content) {
date_filter.start = content.start; date_filter.start = content.start;
date_filter.end = content.end; date_filter.end = content.end;
date_filter.timestamp = content.timestamp; date_filter.timestamp = content.timestamp;

View File

@ -6,6 +6,7 @@ use collab_database::views::RowOrder;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode; use flowy_error::ErrorCode;
use lib_infra::validator_fn::required_not_empty_str; use lib_infra::validator_fn::required_not_empty_str;
use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
use crate::entities::parser::NotEmptyStr; use crate::entities::parser::NotEmptyStr;
@ -49,7 +50,7 @@ impl From<RowOrder> for RowPB {
} }
} }
#[derive(Debug, Default, Clone, ProtoBuf)] #[derive(Debug, Default, Clone, ProtoBuf, Serialize, Deserialize)]
pub struct RowMetaPB { pub struct RowMetaPB {
#[pb(index = 1)] #[pb(index = 1)]
pub id: String, pub id: String,

View File

@ -1078,7 +1078,7 @@ impl DatabaseEditor {
pub async fn group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { pub async fn group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> {
let view = self.database_views.get_view_editor(view_id).await?; let view = self.database_views.get_view_editor(view_id).await?;
view.v_grouping_by_field(field_id).await?; view.v_group_by_field(field_id).await?;
Ok(()) Ok(())
} }

View File

@ -1,12 +1,12 @@
#![allow(clippy::while_let_loop)] #![allow(clippy::while_let_loop)]
use crate::entities::{ use crate::entities::{
CalculationChangesetNotificationPB, DatabaseViewSettingPB, FilterChangesetNotificationPB, CalculationChangesetNotificationPB, DatabaseViewSettingPB, FilterChangesetNotificationPB,
GroupChangesPB, GroupRowsNotificationPB, ReorderAllRowsPB, ReorderSingleRowPB, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, ReorderAllRowsPB, ReorderSingleRowPB,
RowsVisibilityChangePB, SortChangesetNotificationPB, RowMetaPB, RowsChangePB, RowsVisibilityChangePB, SortChangesetNotificationPB,
}; };
use crate::notification::{send_notification, DatabaseNotification}; use crate::notification::{send_notification, DatabaseNotification};
use crate::services::filter::FilterResultNotification; use crate::services::filter::FilterResultNotification;
use crate::services::sort::{InsertSortedRowResult, ReorderAllRowsResult, ReorderSingleRowResult}; use crate::services::sort::{InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult};
use async_stream::stream; use async_stream::stream;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use tokio::sync::broadcast; use tokio::sync::broadcast;
@ -16,7 +16,7 @@ pub enum DatabaseViewChanged {
FilterNotification(FilterResultNotification), FilterNotification(FilterResultNotification),
ReorderAllRowsNotification(ReorderAllRowsResult), ReorderAllRowsNotification(ReorderAllRowsResult),
ReorderSingleRowNotification(ReorderSingleRowResult), ReorderSingleRowNotification(ReorderSingleRowResult),
InsertSortedRowNotification(InsertSortedRowResult), InsertRowNotification(InsertRowResult),
CalculationValueNotification(CalculationChangesetNotificationPB), CalculationValueNotification(CalculationChangesetNotificationPB),
} }
@ -79,7 +79,17 @@ impl DatabaseViewChangedReceiverRunner {
.payload(reorder_row) .payload(reorder_row)
.send() .send()
}, },
DatabaseViewChanged::InsertSortedRowNotification(_result) => {}, DatabaseViewChanged::InsertRowNotification(result) => {
let inserted_row = InsertedRowPB {
row_meta: RowMetaPB::from(result.row),
index: Some(result.index as i32),
is_new: true,
};
let changes = RowsChangePB::from_insert(inserted_row);
send_notification(&result.view_id, DatabaseNotification::DidUpdateViewRows)
.payload(changes)
.send();
},
DatabaseViewChanged::CalculationValueNotification(notification) => send_notification( DatabaseViewChanged::CalculationValueNotification(notification) => send_notification(
&notification.view_id, &notification.view_id,
DatabaseNotification::DidUpdateCalculation, DatabaseNotification::DidUpdateCalculation,

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use collab_database::database::{gen_database_calculation_id, gen_database_sort_id, gen_row_id}; use collab_database::database::{gen_database_calculation_id, gen_database_sort_id, gen_row_id};
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::Field;
use collab_database::rows::{Cells, Row, RowDetail, RowId}; use collab_database::rows::{Cells, Row, RowDetail, RowId};
use collab_database::views::{DatabaseLayout, DatabaseView}; use collab_database::views::{DatabaseLayout, DatabaseView};
use lib_infra::util::timestamp; use lib_infra::util::timestamp;
@ -16,9 +16,9 @@ use lib_dispatch::prelude::af_spawn;
use crate::entities::{ use crate::entities::{
CalendarEventPB, CreateRowParams, CreateRowPayloadPB, DatabaseLayoutMetaPB, CalendarEventPB, CreateRowParams, CreateRowPayloadPB, DatabaseLayoutMetaPB,
DatabaseLayoutSettingPB, DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, DatabaseLayoutSettingPB, DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB,
GroupPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, GroupPB, LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB,
RemoveCalculationChangesetPB, ReorderSortPayloadPB, RowMetaPB, RowsChangePB, ReorderSortPayloadPB, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB,
SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, UpdateSortPayloadPB, UpdateCalculationChangesetPB, UpdateSortPayloadPB,
}; };
use crate::notification::{send_notification, DatabaseNotification}; use crate::notification::{send_notification, DatabaseNotification};
use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController}; use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController};
@ -26,7 +26,7 @@ use crate::services::cell::{CellBuilder, CellCache};
use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow}; use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow};
use crate::services::database_view::view_filter::make_filter_controller; use crate::services::database_view::view_filter::make_filter_controller;
use crate::services::database_view::view_group::{ use crate::services::database_view::view_group::{
get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field, get_cell_for_row, get_cells_for_field, new_group_controller,
}; };
use crate::services::database_view::view_operation::DatabaseViewOperation; use crate::services::database_view::view_operation::DatabaseViewOperation;
use crate::services::database_view::view_sort::make_sort_controller; use crate::services::database_view::view_sort::make_sort_controller;
@ -68,10 +68,6 @@ impl DatabaseViewEditor {
) -> FlowyResult<Self> { ) -> FlowyResult<Self> {
let (notifier, _) = broadcast::channel(100); let (notifier, _) = broadcast::channel(100);
af_spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); af_spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run());
// Group
let group_controller = Arc::new(RwLock::new(
new_group_controller(view_id.clone(), delegate.clone()).await?,
));
// Filter // Filter
let filter_controller = make_filter_controller( let filter_controller = make_filter_controller(
@ -92,6 +88,17 @@ impl DatabaseViewEditor {
) )
.await; .await;
// Group
let group_controller = Arc::new(RwLock::new(
new_group_controller(
view_id.clone(),
delegate.clone(),
filter_controller.clone(),
None,
)
.await?,
));
// Calculations // Calculations
let calculations_controller = let calculations_controller =
make_calculations_controller(&view_id, delegate.clone(), notifier.clone()).await; make_calculations_controller(&view_id, delegate.clone(), notifier.clone()).await;
@ -142,7 +149,7 @@ impl DatabaseViewEditor {
if let Some(controller) = self.group_controller.read().await.as_ref() { if let Some(controller) = self.group_controller.read().await.as_ref() {
let field = self let field = self
.delegate .delegate
.get_field(controller.field_id()) .get_field(controller.get_grouping_field_id())
.ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?; .ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?;
controller.will_create_row(&mut cells, &field, &group_id); controller.will_create_row(&mut cells, &field, &group_id);
} }
@ -168,24 +175,20 @@ impl DatabaseViewEditor {
pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) { pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) {
// Send the group notification if the current view has groups // Send the group notification if the current view has groups
if let Some(controller) = self.group_controller.write().await.as_mut() { if let Some(controller) = self.group_controller.write().await.as_mut() {
let changesets = controller.did_create_row(row_detail, index); let mut row_details = vec![Arc::new(row_detail.clone())];
self.v_filter_rows(&mut row_details).await;
for changeset in changesets { if let Some(row_detail) = row_details.pop() {
notify_did_update_group_rows(changeset).await; let changesets = controller.did_create_row(&row_detail, index);
for changeset in changesets {
notify_did_update_group_rows(changeset).await;
}
} }
} }
let inserted_row = InsertedRowPB {
row_meta: RowMetaPB::from(row_detail),
index: Some(index as i32),
is_new: true,
};
let changes = RowsChangePB::from_insert(inserted_row);
send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
.payload(changes)
.send();
self self
.gen_did_create_row_view_tasks(row_detail.row.clone()) .gen_did_create_row_view_tasks(index, row_detail.clone())
.await; .await;
} }
@ -239,34 +242,41 @@ impl DatabaseViewEditor {
row_detail: &RowDetail, row_detail: &RowDetail,
field_id: String, field_id: String,
) { ) {
let result = self if let Some(controller) = self.group_controller.write().await.as_mut() {
.mut_group_controller(|group_controller, field| { let field = self.delegate.get_field(controller.get_grouping_field_id());
Ok(group_controller.did_update_group_row(old_row, row_detail, &field))
})
.await;
if let Some(Ok(result)) = result { if let Some(field) = field {
let mut group_changes = GroupChangesPB { let mut row_details = vec![Arc::new(row_detail.clone())];
view_id: self.view_id.clone(), self.v_filter_rows(&mut row_details).await;
..Default::default()
};
if let Some(inserted_group) = result.inserted_group {
tracing::trace!("Create group after editing the row: {:?}", inserted_group);
group_changes.inserted_groups.push(inserted_group);
}
if let Some(delete_group) = result.deleted_group {
tracing::trace!("Delete group after editing the row: {:?}", delete_group);
group_changes.deleted_groups.push(delete_group.group_id);
}
if !group_changes.is_empty() { if let Some(row_detail) = row_details.pop() {
notify_did_update_num_of_groups(&self.view_id, group_changes).await; let result = controller.did_update_group_row(old_row, &row_detail, &field);
}
for changeset in result.row_changesets { if let Ok(result) = result {
if !changeset.is_empty() { let mut group_changes = GroupChangesPB {
tracing::trace!("Group change after editing the row: {:?}", changeset); view_id: self.view_id.clone(),
notify_did_update_group_rows(changeset).await; ..Default::default()
};
if let Some(inserted_group) = result.inserted_group {
tracing::trace!("Create group after editing the row: {:?}", inserted_group);
group_changes.inserted_groups.push(inserted_group);
}
if let Some(delete_group) = result.deleted_group {
tracing::trace!("Delete group after editing the row: {:?}", delete_group);
group_changes.deleted_groups.push(delete_group.group_id);
}
if !group_changes.is_empty() {
notify_did_update_num_of_groups(&self.view_id, group_changes).await;
}
for changeset in result.row_changesets {
if !changeset.is_empty() {
tracing::trace!("Group change after editing the row: {:?}", changeset);
notify_did_update_group_rows(changeset).await;
}
}
}
} }
} }
} }
@ -376,7 +386,7 @@ impl DatabaseViewEditor {
pub async fn is_grouping_field(&self, field_id: &str) -> bool { pub async fn is_grouping_field(&self, field_id: &str) -> bool {
match self.group_controller.read().await.as_ref() { match self.group_controller.read().await.as_ref() {
Some(group_controller) => group_controller.field_id() == field_id, Some(group_controller) => group_controller.get_grouping_field_id() == field_id,
None => false, None => false,
} }
} }
@ -385,7 +395,7 @@ impl DatabaseViewEditor {
pub async fn v_initialize_new_group(&self, field_id: &str) -> FlowyResult<()> { pub async fn v_initialize_new_group(&self, field_id: &str) -> FlowyResult<()> {
let is_grouping_field = self.is_grouping_field(field_id).await; let is_grouping_field = self.is_grouping_field(field_id).await;
if !is_grouping_field { if !is_grouping_field {
self.v_grouping_by_field(field_id).await?; self.v_group_by_field(field_id).await?;
if let Some(view) = self.delegate.get_view(&self.view_id).await { if let Some(view) = self.delegate.get_view(&self.view_id).await {
let setting = database_view_setting_pb_from_view(view); let setting = database_view_setting_pb_from_view(view);
@ -399,7 +409,7 @@ impl DatabaseViewEditor {
let mut old_field: Option<Field> = None; let mut old_field: Option<Field> = None;
let result = if let Some(controller) = self.group_controller.write().await.as_mut() { let result = if let Some(controller) = self.group_controller.write().await.as_mut() {
let create_group_results = controller.create_group(name.to_string())?; let create_group_results = controller.create_group(name.to_string())?;
old_field = self.delegate.get_field(controller.field_id()); old_field = self.delegate.get_field(controller.get_grouping_field_id());
create_group_results create_group_results
} else { } else {
(None, None) (None, None)
@ -432,7 +442,7 @@ impl DatabaseViewEditor {
None => return Ok(RowsChangePB::default()), None => return Ok(RowsChangePB::default()),
}; };
let old_field = self.delegate.get_field(controller.field_id()); let old_field = self.delegate.get_field(controller.get_grouping_field_id());
let (row_ids, type_option_data) = controller.delete_group(group_id)?; let (row_ids, type_option_data) = controller.delete_group(group_id)?;
drop(group_controller); drop(group_controller);
@ -462,12 +472,15 @@ impl DatabaseViewEditor {
} }
pub async fn v_update_group(&self, changeset: Vec<GroupChangeset>) -> FlowyResult<()> { pub async fn v_update_group(&self, changeset: Vec<GroupChangeset>) -> FlowyResult<()> {
let mut type_option_data = TypeOptionData::new(); let mut type_option_data = None;
let (old_field, updated_groups) = let (old_field, updated_groups) =
if let Some(controller) = self.group_controller.write().await.as_mut() { if let Some(controller) = self.group_controller.write().await.as_mut() {
let old_field = self.delegate.get_field(controller.field_id()); let old_field = self.delegate.get_field(controller.get_grouping_field_id());
let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset)?; let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset)?;
type_option_data.extend(new_type_option);
if new_type_option.is_some() {
type_option_data = new_type_option;
}
(old_field, updated_groups) (old_field, updated_groups)
} else { } else {
@ -475,7 +488,7 @@ impl DatabaseViewEditor {
}; };
if let Some(old_field) = old_field { if let Some(old_field) = old_field {
if !type_option_data.is_empty() { if let Some(type_option_data) = type_option_data {
self self
.delegate .delegate
.update_field(type_option_data, old_field) .update_field(type_option_data, old_field)
@ -644,15 +657,20 @@ impl DatabaseViewEditor {
#[tracing::instrument(level = "trace", skip(self), err)] #[tracing::instrument(level = "trace", skip(self), err)]
pub async fn v_modify_filters(&self, changeset: FilterChangeset) -> FlowyResult<()> { pub async fn v_modify_filters(&self, changeset: FilterChangeset) -> FlowyResult<()> {
let filter_controller = self.filter_controller.clone(); let notification = self.filter_controller.apply_changeset(changeset).await;
// 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; notify_did_update_filter(notification).await;
let group_controller_read_guard = self.group_controller.read().await;
let grouping_field_id = group_controller_read_guard
.as_ref()
.map(|controller| controller.get_grouping_field_id().to_string());
drop(group_controller_read_guard);
if let Some(field_id) = grouping_field_id {
self.v_group_by_field(&field_id).await?;
}
Ok(()) Ok(())
} }
@ -786,11 +804,6 @@ impl DatabaseViewEditor {
#[tracing::instrument(level = "trace", skip_all, err)] #[tracing::instrument(level = "trace", skip_all, err)]
pub async fn v_did_update_field_type_option(&self, old_field: &Field) -> FlowyResult<()> { pub async fn v_did_update_field_type_option(&self, old_field: &Field) -> FlowyResult<()> {
let field_id = &old_field.id; let field_id = &old_field.id;
// If the id of the grouping field is equal to the updated field's id, then we need to
// update the group setting
if self.is_grouping_field(field_id).await {
self.v_grouping_by_field(field_id).await?;
}
if let Some(field) = self.delegate.get_field(field_id) { if let Some(field) = self.delegate.get_field(field_id) {
self self
@ -808,36 +821,58 @@ impl DatabaseViewEditor {
notify_did_update_filter(notification).await; notify_did_update_filter(notification).await;
} }
} }
// If the id of the grouping field is equal to the updated field's id, then we need to
// update the group setting
if self.is_grouping_field(field_id).await {
self.v_group_by_field(field_id).await?;
}
Ok(()) Ok(())
} }
/// Called when a grouping field is updated. /// Called when a grouping field is updated.
#[tracing::instrument(level = "debug", skip_all, err)] #[tracing::instrument(level = "debug", skip_all, err)]
pub async fn v_grouping_by_field(&self, field_id: &str) -> FlowyResult<()> { pub async fn v_group_by_field(&self, field_id: &str) -> FlowyResult<()> {
if let Some(field) = self.delegate.get_field(field_id) { if let Some(field) = self.delegate.get_field(field_id) {
let new_group_controller = tracing::trace!("create new group controller");
new_group_controller_with_field(self.view_id.clone(), self.delegate.clone(), field).await?;
let new_groups = new_group_controller let new_group_controller = new_group_controller(
.get_all_groups() self.view_id.clone(),
.into_iter() self.delegate.clone(),
.map(|group| GroupPB::from(group.clone())) self.filter_controller.clone(),
.collect(); Some(field),
)
.await?;
*self.group_controller.write().await = Some(new_group_controller); if let Some(controller) = &new_group_controller {
let changeset = GroupChangesPB { let new_groups = controller
view_id: self.view_id.clone(), .get_all_groups()
initial_groups: new_groups, .into_iter()
..Default::default() .map(|group| GroupPB::from(group.clone()))
}; .collect();
debug_assert!(!changeset.is_empty()); let changeset = GroupChangesPB {
if !changeset.is_empty() { view_id: self.view_id.clone(),
send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField) initial_groups: new_groups,
.payload(changeset) ..Default::default()
.send(); };
tracing::trace!("notify did group by field1");
debug_assert!(!changeset.is_empty());
if !changeset.is_empty() {
send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField)
.payload(changeset)
.send();
}
} }
tracing::trace!("notify did group by field2");
*self.group_controller.write().await = new_group_controller;
tracing::trace!("did write group_controller to cache");
} }
Ok(()) Ok(())
} }
@ -958,8 +993,13 @@ impl DatabaseViewEditor {
} }
// initialize the group controller if the current layout support grouping // initialize the group controller if the current layout support grouping
*self.group_controller.write().await = *self.group_controller.write().await = new_group_controller(
new_group_controller(self.view_id.clone(), self.delegate.clone()).await?; self.view_id.clone(),
self.delegate.clone(),
self.filter_controller.clone(),
None,
)
.await?;
let payload = DatabaseLayoutMetaPB { let payload = DatabaseLayoutMetaPB {
view_id: self.view_id.clone(), view_id: self.view_id.clone(),
@ -1019,7 +1059,7 @@ impl DatabaseViewEditor {
.read() .read()
.await .await
.as_ref() .as_ref()
.map(|group| group.field_id().to_owned())?; .map(|controller| controller.get_grouping_field_id().to_owned())?;
let field = self.delegate.get_field(&group_field_id)?; let field = self.delegate.get_field(&group_field_id)?;
let mut write_guard = self.group_controller.write().await; let mut write_guard = self.group_controller.write().await;
if let Some(group_controller) = &mut *write_guard { if let Some(group_controller) = &mut *write_guard {
@ -1054,7 +1094,7 @@ impl DatabaseViewEditor {
}); });
} }
async fn gen_did_create_row_view_tasks(&self, row: Row) { async fn gen_did_create_row_view_tasks(&self, preliminary_index: usize, row_detail: RowDetail) {
let weak_sort_controller = Arc::downgrade(&self.sort_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller);
let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); let weak_calculations_controller = Arc::downgrade(&self.calculations_controller);
af_spawn(async move { af_spawn(async move {
@ -1062,12 +1102,14 @@ impl DatabaseViewEditor {
sort_controller sort_controller
.read() .read()
.await .await
.did_create_row(row.id.clone()) .did_create_row(preliminary_index, &row_detail)
.await; .await;
} }
if let Some(calculations_controller) = weak_calculations_controller.upgrade() { if let Some(calculations_controller) = weak_calculations_controller.upgrade() {
calculations_controller.did_receive_row_changed(row).await; calculations_controller
.did_receive_row_changed(row_detail.row.clone())
.await;
} }
}); });
} }

View File

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::rows::RowId; use collab_database::rows::{RowDetail, RowId};
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
use lib_infra::future::{to_fut, Fut}; use lib_infra::future::{to_fut, Fut};
@ -9,60 +9,61 @@ use lib_infra::future::{to_fut, Fut};
use crate::entities::FieldType; use crate::entities::FieldType;
use crate::services::database_view::DatabaseViewOperation; use crate::services::database_view::DatabaseViewOperation;
use crate::services::field::RowSingleCellData; use crate::services::field::RowSingleCellData;
use crate::services::filter::FilterController;
use crate::services::group::{ use crate::services::group::{
find_suitable_grouping_field, make_group_controller, GroupContextDelegate, GroupController, make_group_controller, GroupContextDelegate, GroupController, GroupControllerDelegate,
GroupControllerDelegate, GroupSetting, GroupSetting,
}; };
pub async fn new_group_controller_with_field(
view_id: String,
delegate: Arc<dyn DatabaseViewOperation>,
grouping_field: Field,
) -> FlowyResult<Box<dyn GroupController>> {
let configuration_delegate = GroupControllerDelegateImpl(delegate.clone());
let rows = delegate.get_rows(&view_id).await;
make_group_controller(view_id, grouping_field, rows, configuration_delegate).await
}
pub async fn new_group_controller( pub async fn new_group_controller(
view_id: String, view_id: String,
delegate: Arc<dyn DatabaseViewOperation>, delegate: Arc<dyn DatabaseViewOperation>,
filter_controller: Arc<FilterController>,
grouping_field: Option<Field>,
) -> FlowyResult<Option<Box<dyn GroupController>>> { ) -> FlowyResult<Option<Box<dyn GroupController>>> {
let fields = delegate.get_fields(&view_id, None).await; if !delegate.get_layout_for_view(&view_id).is_board() {
let controller_delegate = GroupControllerDelegateImpl(delegate.clone()); return Ok(None);
// Read the grouping field or find a new grouping field
let mut grouping_field = controller_delegate
.get_group_setting(&view_id)
.await
.and_then(|setting| {
fields
.iter()
.find(|field| field.id == setting.field_id)
.cloned()
});
let layout = delegate.get_layout_for_view(&view_id);
// If the view is a board and the grouping field is empty, we need to find a new grouping field
if layout.is_board() && grouping_field.is_none() {
grouping_field = find_suitable_grouping_field(&fields);
} }
if let Some(grouping_field) = grouping_field { let controller_delegate = GroupControllerDelegateImpl {
let rows = delegate.get_rows(&view_id).await; delegate: delegate.clone(),
Ok(Some( filter_controller: filter_controller.clone(),
make_group_controller(view_id, grouping_field, rows, controller_delegate).await?, };
))
} else { let grouping_field = match grouping_field {
Ok(None) Some(field) => Some(field),
} None => {
let group_setting = controller_delegate.get_group_setting(&view_id).await;
let fields = delegate.get_fields(&view_id, None).await;
group_setting
.and_then(|setting| {
fields
.iter()
.find(|field| field.id == setting.field_id)
.cloned()
})
.or_else(|| find_suitable_grouping_field(&fields))
},
};
let controller = match grouping_field {
Some(field) => Some(make_group_controller(&view_id, field, controller_delegate).await?),
None => None,
};
Ok(controller)
} }
pub(crate) struct GroupControllerDelegateImpl(pub Arc<dyn DatabaseViewOperation>); pub(crate) struct GroupControllerDelegateImpl {
delegate: Arc<dyn DatabaseViewOperation>,
filter_controller: Arc<FilterController>,
}
impl GroupContextDelegate for GroupControllerDelegateImpl { impl GroupContextDelegate for GroupControllerDelegateImpl {
fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>> { fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>> {
let mut settings = self.0.get_group_setting(view_id); let mut settings = self.delegate.get_group_setting(view_id);
to_fut(async move { to_fut(async move {
if settings.is_empty() { if settings.is_empty() {
None None
@ -75,19 +76,30 @@ impl GroupContextDelegate for GroupControllerDelegateImpl {
fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>> { fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>> {
let field_id = field_id.to_owned(); let field_id = field_id.to_owned();
let view_id = view_id.to_owned(); let view_id = view_id.to_owned();
let delegate = self.0.clone(); let delegate = self.delegate.clone();
to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await }) to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await })
} }
fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>> { fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>> {
self.0.insert_group_setting(view_id, group_setting); self.delegate.insert_group_setting(view_id, group_setting);
to_fut(async move { Ok(()) }) to_fut(async move { Ok(()) })
} }
} }
impl GroupControllerDelegate for GroupControllerDelegateImpl { impl GroupControllerDelegate for GroupControllerDelegateImpl {
fn get_field(&self, field_id: &str) -> Option<Field> { fn get_field(&self, field_id: &str) -> Option<Field> {
self.0.get_field(field_id) self.delegate.get_field(field_id)
}
fn get_all_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> {
let view_id = view_id.to_string();
let delegate = self.delegate.clone();
let filter_controller = self.filter_controller.clone();
to_fut(async move {
let mut row_details = delegate.get_rows(&view_id).await;
filter_controller.filter_rows(&mut row_details).await;
row_details
})
} }
} }
@ -143,3 +155,15 @@ pub(crate) async fn get_cells_for_field(
vec![] vec![]
} }
fn find_suitable_grouping_field(fields: &[Field]) -> Option<Field> {
let groupable_field = fields
.iter()
.find(|field| FieldType::from(field.field_type).can_be_group());
if let Some(field) = groupable_field {
Some(field.clone())
} else {
fields.iter().find(|field| field.is_primary).cloned()
}
}

View File

@ -70,6 +70,16 @@ impl SortDelegate for DatabaseViewSortDelegateImpl {
}) })
} }
fn filter_row(&self, row_detail: &RowDetail) -> Fut<bool> {
let filter_controller = self.filter_controller.clone();
let row_detail = row_detail.clone();
to_fut(async move {
let mut row_details = vec![Arc::new(row_detail)];
filter_controller.filter_rows(&mut row_details).await;
!row_details.is_empty()
})
}
fn get_field(&self, field_id: &str) -> Option<Field> { fn get_field(&self, field_id: &str) -> Option<Field> {
self.delegate.get_field(field_id) self.delegate.get_field(field_id)
} }

View File

@ -71,7 +71,7 @@ impl From<URLCellDataPB> for URLCellData {
impl AsRef<str> for URLCellData { impl AsRef<str> for URLCellData {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
&self.url &self.data
} }
} }

View File

@ -10,7 +10,7 @@ use flowy_error::{FlowyError, FlowyResult};
use lib_infra::box_any::BoxAny; use lib_infra::box_any::BoxAny;
use crate::entities::{ use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType, FilterType, CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType,
InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB, InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
}; };
use crate::services::field::SelectOptionIds; use crate::services::field::SelectOptionIds;
@ -337,7 +337,7 @@ impl<'a> From<&'a Filter> for FilterMap {
}, },
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
let filter = condition_and_content.cloned::<DateFilterPB>()?; let filter = condition_and_content.cloned::<DateFilterPB>()?;
let content = DateFilterContentPB { let content = DateFilterContent {
start: filter.start, start: filter.start,
end: filter.end, end: filter.end,
timestamp: filter.timestamp, timestamp: filter.timestamp,

View File

@ -79,8 +79,9 @@ pub trait GroupCustomize: Send + Sync {
fn update_type_option_when_update_group( fn update_type_option_when_update_group(
&mut self, &mut self,
_changeset: &GroupChangeset, _changeset: &GroupChangeset,
_type_option: &mut Self::GroupTypeOption, _type_option: &Self::GroupTypeOption,
) { ) -> Option<Self::GroupTypeOption> {
None
} }
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str); fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str);
@ -97,7 +98,7 @@ pub trait GroupCustomize: Send + Sync {
/// ///
pub trait GroupController: Send + Sync { pub trait GroupController: Send + Sync {
/// Returns the id of field that is being used to group the rows /// Returns the id of field that is being used to group the rows
fn field_id(&self) -> &str; fn get_grouping_field_id(&self) -> &str;
/// Returns all of the groups currently managed by the controller /// Returns all of the groups currently managed by the controller
fn get_all_groups(&self) -> Vec<&GroupData>; fn get_all_groups(&self) -> Vec<&GroupData>;
@ -189,7 +190,7 @@ pub trait GroupController: Send + Sync {
fn apply_group_changeset( fn apply_group_changeset(
&mut self, &mut self,
changesets: &[GroupChangeset], changesets: &[GroupChangeset],
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)>; ) -> FlowyResult<(Vec<GroupPB>, Option<TypeOptionData>)>;
/// Called before the row was created. /// Called before the row was created.
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str); fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str);

View File

@ -4,6 +4,7 @@ use std::sync::Arc;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cells, Row, RowDetail, RowId}; use collab_database::rows::{Cells, Row, RowDetail, RowId};
use futures::executor::block_on; use futures::executor::block_on;
use lib_infra::future::Fut;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
@ -24,6 +25,8 @@ use crate::services::group::{GroupChangeset, GroupsBuilder, MoveGroupRowContext}
pub trait GroupControllerDelegate: Send + Sync + 'static { pub trait GroupControllerDelegate: Send + Sync + 'static {
fn get_field(&self, field_id: &str) -> Option<Field>; fn get_field(&self, field_id: &str) -> Option<Field>;
fn get_all_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>;
} }
/// [BaseGroupController] is a generic group controller that provides customized implementations /// [BaseGroupController] is a generic group controller that provides customized implementations
@ -159,7 +162,7 @@ where
G: GroupsBuilder<Context = GroupControllerContext<C>, GroupTypeOption = T>, G: GroupsBuilder<Context = GroupControllerContext<C>, GroupTypeOption = T>,
Self: GroupCustomize<GroupTypeOption = T>, Self: GroupCustomize<GroupTypeOption = T>,
{ {
fn field_id(&self) -> &str { fn get_grouping_field_id(&self) -> &str {
&self.grouping_field_id &self.grouping_field_id
} }
@ -228,12 +231,13 @@ where
row_detail: &RowDetail, row_detail: &RowDetail,
index: usize, index: usize,
) -> Vec<GroupRowsNotificationPB> { ) -> Vec<GroupRowsNotificationPB> {
let mut changesets: Vec<GroupRowsNotificationPB> = vec![];
let cell = match row_detail.row.cells.get(&self.grouping_field_id) { let cell = match row_detail.row.cells.get(&self.grouping_field_id) {
None => self.placeholder_cell(), None => self.placeholder_cell(),
Some(cell) => Some(cell.clone()), Some(cell) => Some(cell.clone()),
}; };
let mut changesets: Vec<GroupRowsNotificationPB> = vec![];
if let Some(cell) = cell { if let Some(cell) = cell {
let cell_data = <T as TypeOption>::CellData::from(&cell); let cell_data = <T as TypeOption>::CellData::from(&cell);
@ -245,7 +249,7 @@ where
let changeset = GroupRowsNotificationPB::insert( let changeset = GroupRowsNotificationPB::insert(
group.id.clone(), group.id.clone(),
vec![InsertedRowPB { vec![InsertedRowPB {
row_meta: row_detail.into(), row_meta: (*row_detail).clone().into(),
index: Some(index as i32), index: Some(index as i32),
is_new: true, is_new: true,
}], }],
@ -256,15 +260,15 @@ where
if !suitable_group_ids.is_empty() { if !suitable_group_ids.is_empty() {
for group_id in suitable_group_ids.iter() { for group_id in suitable_group_ids.iter() {
if let Some(group) = self.context.get_mut_group(group_id) { if let Some(group) = self.context.get_mut_group(group_id) {
group.add_row(row_detail.clone()); group.add_row((*row_detail).clone());
} }
} }
} else if let Some(no_status_group) = self.context.get_mut_no_status_group() { } else if let Some(no_status_group) = self.context.get_mut_no_status_group() {
no_status_group.add_row(row_detail.clone()); no_status_group.add_row((*row_detail).clone());
let changeset = GroupRowsNotificationPB::insert( let changeset = GroupRowsNotificationPB::insert(
no_status_group.id.clone(), no_status_group.id.clone(),
vec![InsertedRowPB { vec![InsertedRowPB {
row_meta: row_detail.into(), row_meta: (*row_detail).clone().into(),
index: Some(index as i32), index: Some(index as i32),
is_new: true, is_new: true,
}], }],
@ -282,18 +286,12 @@ where
row_detail: &RowDetail, row_detail: &RowDetail,
field: &Field, field: &Field,
) -> FlowyResult<DidUpdateGroupRowResult> { ) -> FlowyResult<DidUpdateGroupRowResult> {
// let cell_data = row_rev.cells.get(&self.field_id).and_then(|cell_rev| {
// let cell_data: Option<P> = get_type_cell_data(cell_rev, field_rev, None);
// cell_data
// });
let mut result = DidUpdateGroupRowResult { let mut result = DidUpdateGroupRowResult {
inserted_group: None, inserted_group: None,
deleted_group: None, deleted_group: None,
row_changesets: vec![], row_changesets: vec![],
}; };
if let Some(cell_data) = get_cell_data_from_row::<P>(Some(&row_detail.row), field) { if let Some(cell_data) = get_cell_data_from_row::<P>(Some(&row_detail.row), field) {
let _old_row = old_row_detail.as_ref();
let old_cell_data = let old_cell_data =
get_cell_data_from_row::<P>(old_row_detail.as_ref().map(|detail| &detail.row), field); get_cell_data_from_row::<P>(old_row_detail.as_ref().map(|detail| &detail.row), field);
if let Ok((insert, delete)) = self.create_or_delete_group_when_cell_changed( if let Ok((insert, delete)) = self.create_or_delete_group_when_cell_changed(
@ -376,7 +374,7 @@ where
} }
fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> { fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> {
let group = if group_id != self.field_id() { let group = if group_id != self.get_grouping_field_id() {
self.get_group(group_id) self.get_group(group_id)
} else { } else {
None None
@ -399,17 +397,26 @@ where
fn apply_group_changeset( fn apply_group_changeset(
&mut self, &mut self,
changeset: &[GroupChangeset], changeset: &[GroupChangeset],
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> { ) -> FlowyResult<(Vec<GroupPB>, Option<TypeOptionData>)> {
// update group visibility
for group_changeset in changeset.iter() { for group_changeset in changeset.iter() {
self.context.update_group(group_changeset)?; self.context.update_group(group_changeset)?;
} }
let mut type_option = self.get_grouping_field_type_option().ok_or_else(|| { // update group name
let type_option = self.get_grouping_field_type_option().ok_or_else(|| {
FlowyError::internal().with_context("Failed to get grouping field type option") FlowyError::internal().with_context("Failed to get grouping field type option")
})?; })?;
let mut updated_type_option = None;
for group_changeset in changeset.iter() { for group_changeset in changeset.iter() {
self.update_type_option_when_update_group(group_changeset, &mut type_option); if let Some(type_option) =
self.update_type_option_when_update_group(group_changeset, &type_option)
{
updated_type_option = Some(type_option);
break;
}
} }
let updated_groups = changeset let updated_groups = changeset
@ -421,7 +428,10 @@ where
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok((updated_groups, type_option.into())) Ok((
updated_groups,
updated_type_option.map(|type_option| type_option.into()),
))
} }
fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) {

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use std::sync::Arc;
use collab_database::fields::{Field, TypeOptionData}; use collab_database::fields::{Field, TypeOptionData};
use collab_database::rows::{Cells, Row, RowDetail, RowId}; use collab_database::rows::{Cells, Row, RowDetail, RowId};
@ -10,7 +11,9 @@ use crate::entities::{
use crate::services::group::action::{ use crate::services::group::action::{
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController, DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController,
}; };
use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext}; use crate::services::group::{
GroupChangeset, GroupControllerDelegate, GroupData, MoveGroupRowContext,
};
/// A [DefaultGroupController] is used to handle the group actions for the [FieldType] that doesn't /// A [DefaultGroupController] is used to handle the group actions for the [FieldType] that doesn't
/// implement its own group controller. The default group controller only contains one group, which /// implement its own group controller. The default group controller only contains one group, which
@ -19,23 +22,24 @@ use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext};
pub struct DefaultGroupController { pub struct DefaultGroupController {
pub field_id: String, pub field_id: String,
pub group: GroupData, pub group: GroupData,
pub delegate: Arc<dyn GroupControllerDelegate>,
} }
const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController"; const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController";
impl DefaultGroupController { impl DefaultGroupController {
pub fn new(field: &Field) -> Self { pub fn new(field: &Field, delegate: Arc<dyn GroupControllerDelegate>) -> Self {
let group = GroupData::new(DEFAULT_GROUP_CONTROLLER.to_owned(), field.id.clone(), true); let group = GroupData::new(DEFAULT_GROUP_CONTROLLER.to_owned(), field.id.clone(), true);
Self { Self {
field_id: field.id.clone(), field_id: field.id.clone(),
group, group,
delegate,
} }
} }
} }
#[async_trait]
impl GroupController for DefaultGroupController { impl GroupController for DefaultGroupController {
fn field_id(&self) -> &str { fn get_grouping_field_id(&self) -> &str {
&self.field_id &self.field_id
} }
@ -70,12 +74,12 @@ impl GroupController for DefaultGroupController {
row_detail: &RowDetail, row_detail: &RowDetail,
index: usize, index: usize,
) -> Vec<GroupRowsNotificationPB> { ) -> Vec<GroupRowsNotificationPB> {
self.group.add_row(row_detail.clone()); self.group.add_row((*row_detail).clone());
vec![GroupRowsNotificationPB::insert( vec![GroupRowsNotificationPB::insert(
self.group.id.clone(), self.group.id.clone(),
vec![InsertedRowPB { vec![InsertedRowPB {
row_meta: row_detail.into(), row_meta: (*row_detail).clone().into(),
index: Some(index as i32), index: Some(index as i32),
is_new: true, is_new: true,
}], }],
@ -128,8 +132,8 @@ impl GroupController for DefaultGroupController {
fn apply_group_changeset( fn apply_group_changeset(
&mut self, &mut self,
_changeset: &[GroupChangeset], _changeset: &[GroupChangeset],
) -> FlowyResult<(Vec<GroupPB>, TypeOptionData)> { ) -> FlowyResult<(Vec<GroupPB>, Option<TypeOptionData>)> {
Ok((Vec::new(), TypeOptionData::default())) Ok((Vec::new(), None))
} }
fn will_create_row(&self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} fn will_create_row(&self, _cells: &mut Cells, _field: &Field, _group_id: &str) {}

View File

@ -124,10 +124,11 @@ impl GroupCustomize for MultiSelectGroupController {
fn update_type_option_when_update_group( fn update_type_option_when_update_group(
&mut self, &mut self,
changeset: &GroupChangeset, changeset: &GroupChangeset,
type_option: &mut Self::GroupTypeOption, type_option: &Self::GroupTypeOption,
) { ) -> Option<Self::GroupTypeOption> {
if let Some(name) = &changeset.name { if let Some(name) = &changeset.name {
let mut new_type_option = type_option.clone(); let mut new_type_option = type_option.clone();
let select_option = type_option let select_option = type_option
.options .options
.iter() .iter()
@ -139,6 +140,10 @@ impl GroupCustomize for MultiSelectGroupController {
..select_option.to_owned() ..select_option.to_owned()
}; };
new_type_option.insert_option(new_select_option); new_type_option.insert_option(new_select_option);
Some(new_type_option)
} else {
None
} }
} }

View File

@ -126,10 +126,11 @@ impl GroupCustomize for SingleSelectGroupController {
fn update_type_option_when_update_group( fn update_type_option_when_update_group(
&mut self, &mut self,
changeset: &GroupChangeset, changeset: &GroupChangeset,
type_option: &mut Self::GroupTypeOption, type_option: &Self::GroupTypeOption,
) { ) -> Option<Self::GroupTypeOption> {
if let Some(name) = &changeset.name { if let Some(name) = &changeset.name {
let mut new_type_option = type_option.clone(); let mut new_type_option = type_option.clone();
let select_option = type_option let select_option = type_option
.options .options
.iter() .iter()
@ -141,6 +142,10 @@ impl GroupCustomize for SingleSelectGroupController {
..select_option.to_owned() ..select_option.to_owned()
}; };
new_type_option.insert_option(new_select_option); new_type_option.insert_option(new_select_option);
Some(new_type_option)
} else {
None
} }
} }

View File

@ -86,9 +86,8 @@ impl RowChangeset {
err err
)] )]
pub async fn make_group_controller<D>( pub async fn make_group_controller<D>(
view_id: String, view_id: &str,
grouping_field: Field, grouping_field: Field,
row_details: Vec<Arc<RowDetail>>,
delegate: D, delegate: D,
) -> FlowyResult<Box<dyn GroupController>> ) -> FlowyResult<Box<dyn GroupController>>
where where
@ -102,68 +101,81 @@ where
match grouping_field_type { match grouping_field_type {
FieldType::SingleSelect => { FieldType::SingleSelect => {
let configuration = let configuration = SingleSelectGroupControllerContext::new(
SingleSelectGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()) view_id.to_string(),
.await?; grouping_field.clone(),
delegate.clone(),
)
.await?;
let controller = let controller =
SingleSelectGroupController::new(&grouping_field, configuration, delegate).await?; SingleSelectGroupController::new(&grouping_field, configuration, delegate.clone()).await?;
group_controller = Box::new(controller); group_controller = Box::new(controller);
}, },
FieldType::MultiSelect => { FieldType::MultiSelect => {
let configuration = let configuration = MultiSelectGroupControllerContext::new(
MultiSelectGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()) view_id.to_string(),
.await?; grouping_field.clone(),
delegate.clone(),
)
.await?;
let controller = let controller =
MultiSelectGroupController::new(&grouping_field, configuration, delegate).await?; MultiSelectGroupController::new(&grouping_field, configuration, delegate.clone()).await?;
group_controller = Box::new(controller); group_controller = Box::new(controller);
}, },
FieldType::Checkbox => { FieldType::Checkbox => {
let configuration = let configuration = CheckboxGroupControllerContext::new(
CheckboxGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()) view_id.to_string(),
.await?; grouping_field.clone(),
delegate.clone(),
)
.await?;
let controller = let controller =
CheckboxGroupController::new(&grouping_field, configuration, delegate).await?; CheckboxGroupController::new(&grouping_field, configuration, delegate.clone()).await?;
group_controller = Box::new(controller); group_controller = Box::new(controller);
}, },
FieldType::URL => { FieldType::URL => {
let configuration = let configuration = URLGroupControllerContext::new(
URLGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()).await?; view_id.to_string(),
let controller = URLGroupController::new(&grouping_field, configuration, delegate).await?; grouping_field.clone(),
delegate.clone(),
)
.await?;
let controller =
URLGroupController::new(&grouping_field, configuration, delegate.clone()).await?;
group_controller = Box::new(controller); group_controller = Box::new(controller);
}, },
FieldType::DateTime => { FieldType::DateTime => {
let configuration = let configuration = DateGroupControllerContext::new(
DateGroupControllerContext::new(view_id, grouping_field.clone(), delegate.clone()).await?; view_id.to_string(),
let controller = DateGroupController::new(&grouping_field, configuration, delegate).await?; grouping_field.clone(),
delegate.clone(),
)
.await?;
let controller =
DateGroupController::new(&grouping_field, configuration, delegate.clone()).await?;
group_controller = Box::new(controller); group_controller = Box::new(controller);
}, },
_ => { _ => {
group_controller = Box::new(DefaultGroupController::new(&grouping_field)); group_controller = Box::new(DefaultGroupController::new(
&grouping_field,
delegate.clone(),
));
}, },
} }
// Separates the rows into different groups // Separates the rows into different groups
let row_details = delegate.get_all_rows(view_id).await;
let rows = row_details let rows = row_details
.iter() .iter()
.map(|row| row.as_ref()) .map(|row| row.as_ref())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
group_controller.fill_groups(rows.as_slice(), &grouping_field)?; group_controller.fill_groups(rows.as_slice(), &grouping_field)?;
Ok(group_controller) Ok(group_controller)
} }
#[tracing::instrument(level = "debug", skip_all)]
pub fn find_suitable_grouping_field(fields: &[Field]) -> Option<Field> {
let groupable_field = fields
.iter()
.find(|field| FieldType::from(field.field_type).can_be_group());
if let Some(field) = groupable_field {
Some(field.clone())
} else {
fields.iter().find(|field| field.is_primary).cloned()
}
}
/// Returns a `default` group configuration for the [Field] /// Returns a `default` group configuration for the [Field]
/// ///
/// # Arguments /// # Arguments

View File

@ -21,14 +21,14 @@ use crate::services::field::{
default_order, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellExt, default_order, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellExt,
}; };
use crate::services::sort::{ use crate::services::sort::{
InsertSortedRowResult, ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition,
SortCondition,
}; };
pub trait SortDelegate: Send + Sync { pub trait SortDelegate: Send + Sync {
fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut<Option<Arc<Sort>>>; fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut<Option<Arc<Sort>>>;
/// Returns all the rows after applying grid's filter /// Returns all the rows after applying grid's filter
fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>; fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>;
fn filter_row(&self, row_detail: &RowDetail) -> Fut<bool>;
fn get_field(&self, field_id: &str) -> Option<Field>; fn get_field(&self, field_id: &str) -> Option<Field>;
fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>>; fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>>;
} }
@ -94,14 +94,27 @@ impl SortController {
} }
} }
pub async fn did_create_row(&self, row_id: RowId) { pub async fn did_create_row(&self, preliminary_index: usize, row_detail: &RowDetail) {
if !self.delegate.filter_row(row_detail).await {
return;
}
if !self.sorts.is_empty() { if !self.sorts.is_empty() {
self self
.gen_task( .gen_task(
SortEvent::NewRowInserted(row_id), SortEvent::NewRowInserted(row_detail.clone()),
QualityOfService::Background, QualityOfService::Background,
) )
.await; .await;
} else {
let result = InsertRowResult {
view_id: self.view_id.clone(),
row: row_detail.clone(),
index: preliminary_index,
};
let _ = self
.notifier
.send(DatabaseViewChanged::InsertRowNotification(result));
} }
} }
@ -162,22 +175,20 @@ impl SortController {
_ => tracing::trace!("The row index cache is outdated"), _ => tracing::trace!("The row index cache is outdated"),
} }
}, },
SortEvent::NewRowInserted(row_id) => { SortEvent::NewRowInserted(row_detail) => {
self.sort_rows(&mut row_details).await; self.sort_rows(&mut row_details).await;
let row_index = self.row_index_cache.get(&row_id).cloned(); let row_index = self.row_index_cache.get(&row_detail.row.id).cloned();
match row_index { match row_index {
Some(row_index) => { Some(row_index) => {
let notification = InsertSortedRowResult { let notification = InsertRowResult {
row_id: row_id.clone(),
view_id: self.view_id.clone(), view_id: self.view_id.clone(),
row: row_detail.clone(),
index: row_index, index: row_index,
}; };
self.row_index_cache.insert(row_id, row_index); self.row_index_cache.insert(row_detail.row.id, row_index);
let _ = self let _ = self
.notifier .notifier
.send(DatabaseViewChanged::InsertSortedRowNotification( .send(DatabaseViewChanged::InsertRowNotification(notification));
notification,
));
}, },
_ => tracing::trace!("The row index cache is outdated"), _ => tracing::trace!("The row index cache is outdated"),
} }
@ -353,7 +364,7 @@ fn cmp_cell(
enum SortEvent { enum SortEvent {
SortDidChanged, SortDidChanged,
RowDidChanged(RowId), RowDidChanged(RowId),
NewRowInserted(RowId), NewRowInserted(RowDetail),
DeleteAllSorts, DeleteAllSorts,
} }

View File

@ -2,7 +2,7 @@ use std::cmp::Ordering;
use anyhow::bail; use anyhow::bail;
use collab::core::any_map::AnyMapExtension; use collab::core::any_map::AnyMapExtension;
use collab_database::rows::RowId; use collab_database::rows::{RowDetail, RowId};
use collab_database::views::{SortMap, SortMapBuilder}; use collab_database::views::{SortMap, SortMapBuilder};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -107,9 +107,9 @@ pub struct ReorderSingleRowResult {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct InsertSortedRowResult { pub struct InsertRowResult {
pub view_id: String, pub view_id: String,
pub row_id: RowId, pub row: RowDetail,
pub index: usize, pub index: usize,
} }

View File

@ -212,7 +212,7 @@ async fn assert_sort_changed(
old_row_orders.insert(changed.new_index, old); old_row_orders.insert(changed.new_index, old);
assert_eq!(old_row_orders, new_row_orders); assert_eq!(old_row_orders, new_row_orders);
}, },
DatabaseViewChanged::InsertSortedRowNotification(_changed) => {}, DatabaseViewChanged::InsertRowNotification(_changed) => {},
_ => {}, _ => {},
} }
}) })