mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: add new group (#3854)
* feat: implement backend logic * fix: did_create_row not working properly * fix: did_delete_group not working properly * fix: test * chore: fix clippy * fix: new card not editable and in wrong position * feat: imlement UI for add new stack * test: add integration test * chore: i18n * chore: remove debug message * chore: merge conflict --------- Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
4d82bb5322
commit
c4fc60612f
@ -1,11 +1,14 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
import '../util/database_test_op.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@ -46,5 +49,56 @@ void main() {
|
||||
await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());
|
||||
expect(find.textContaining(name, findRichText: true), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('add new group', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Board);
|
||||
|
||||
// assert number of groups
|
||||
tester.assertNumberOfGroups(4);
|
||||
|
||||
// scroll the board horizontally to ensure add new group button appears
|
||||
await tester.scrollBoardToEnd();
|
||||
|
||||
// assert and click on add new group button
|
||||
tester.assertNewGroupTextField(false);
|
||||
await tester.tapNewGroupButton();
|
||||
tester.assertNewGroupTextField(true);
|
||||
|
||||
// enter new group name and submit
|
||||
await tester.enterNewGroupName('needs design', submit: true);
|
||||
|
||||
// assert number of groups has increased
|
||||
tester.assertNumberOfGroups(5);
|
||||
|
||||
// assert text field has disappeared
|
||||
await tester.scrollBoardToEnd();
|
||||
tester.assertNewGroupTextField(false);
|
||||
|
||||
// click on add new group button
|
||||
await tester.tapNewGroupButton();
|
||||
tester.assertNewGroupTextField(true);
|
||||
|
||||
// type some things
|
||||
await tester.enterNewGroupName('needs planning', submit: false);
|
||||
|
||||
// click on clear button and assert empty contents
|
||||
await tester.clearNewGroupTextField();
|
||||
|
||||
// press escape to cancel
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
tester.assertNewGroupTextField(false);
|
||||
|
||||
// click on add new group button
|
||||
await tester.tapNewGroupButton();
|
||||
tester.assertNewGroupTextField(true);
|
||||
|
||||
// press elsewhere to cancel
|
||||
await tester.tap(find.byType(AppFlowyBoard));
|
||||
await tester.pumpAndSettle();
|
||||
tester.assertNewGroupTextField(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart';
|
||||
@ -59,6 +60,7 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@ -1390,6 +1392,84 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findCreateButton);
|
||||
}
|
||||
|
||||
void assertNumberOfGroups(int number) {
|
||||
final groups = find.byType(BoardColumnHeader, skipOffstage: false);
|
||||
expect(groups, findsNWidgets(number));
|
||||
}
|
||||
|
||||
Future<void> scrollBoardToEnd() async {
|
||||
final scrollable = find
|
||||
.descendant(
|
||||
of: find.byType(AppFlowyBoard),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is Scrollable && widget.axis == Axis.horizontal,
|
||||
),
|
||||
)
|
||||
.first;
|
||||
await scrollUntilVisible(
|
||||
find.byType(BoardTrailing),
|
||||
300,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapNewGroupButton() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(BoardTrailing),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,
|
||||
),
|
||||
);
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
void assertNewGroupTextField(bool isVisible) {
|
||||
final textField = find.descendant(
|
||||
of: find.byType(BoardTrailing),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
if (isVisible) {
|
||||
expect(textField, findsOneWidget);
|
||||
} else {
|
||||
expect(textField, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enterNewGroupName(String name, {required bool submit}) async {
|
||||
final textField = find.descendant(
|
||||
of: find.byType(BoardTrailing),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
await enterText(textField, name);
|
||||
await pumpAndSettle();
|
||||
if (submit) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearNewGroupTextField() async {
|
||||
final textField = find.descendant(
|
||||
of: find.byType(BoardTrailing),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: textField,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m,
|
||||
),
|
||||
),
|
||||
);
|
||||
final textFieldWidget = widget<TextField>(textField);
|
||||
assert(
|
||||
textFieldWidget.controller != null &&
|
||||
textFieldWidget.controller!.text.isEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapTabBarLinkedViewByViewName(String name) async {
|
||||
final viewButton = findTabBarLinkViewByViewName(name);
|
||||
await tapButton(viewButton);
|
||||
|
@ -37,4 +37,15 @@ class GroupBackendService {
|
||||
}
|
||||
return DatabaseEventUpdateGroup(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> createGroup({
|
||||
required String name,
|
||||
String groupConfigId = "",
|
||||
}) {
|
||||
final payload = CreateGroupPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..name = name;
|
||||
|
||||
return DatabaseEventCreateGroup(payload).send();
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
createGroup: (name) async {
|
||||
final result = await groupBackendSvc.createGroup(name: name);
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
},
|
||||
didCreateRow: (group, row, int? index) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -346,6 +350,7 @@ class BoardEvent with _$BoardEvent {
|
||||
const factory BoardEvent.initial() = _InitialBoard;
|
||||
const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow;
|
||||
const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow;
|
||||
const factory BoardEvent.createGroup(String name) = _CreateGroup;
|
||||
const factory BoardEvent.startEditingHeader(String groupId) =
|
||||
_StartEditingHeader;
|
||||
const factory BoardEvent.endEditingHeader(String groupId, String groupName) =
|
||||
|
@ -16,12 +16,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.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_web.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart' hide Card;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../widgets/card/cells/card_cell.dart';
|
||||
@ -127,6 +127,7 @@ class BoardContent extends StatefulWidget {
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
late AppFlowyBoardScrollController scrollManager;
|
||||
late final ScrollController scrollController;
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
|
||||
final config = const AppFlowyBoardConfig(
|
||||
@ -138,6 +139,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
super.initState();
|
||||
|
||||
scrollManager = AppFlowyBoardScrollController();
|
||||
scrollController = ScrollController();
|
||||
renderHook.addSelectOptionHook((options, groupId, _) {
|
||||
// The cell should hide if the option id is equal to the groupId.
|
||||
final isInGroup =
|
||||
@ -172,7 +174,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: (_, groupData) =>
|
||||
BlocProvider<BoardBloc>.value(
|
||||
@ -183,6 +185,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
trailing: BoardTrailing(scrollController: scrollController),
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
@ -348,3 +351,109 @@ class _BoardContentState extends State<BoardContent> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardTrailing extends StatefulWidget {
|
||||
final ScrollController scrollController;
|
||||
const BoardTrailing({required this.scrollController, super.key});
|
||||
|
||||
@override
|
||||
State<BoardTrailing> createState() => _BoardTrailingState();
|
||||
}
|
||||
|
||||
class _BoardTrailingState extends State<BoardTrailing> {
|
||||
bool isEditing = false;
|
||||
late final TextEditingController _textController;
|
||||
late final FocusNode _focusNode;
|
||||
|
||||
void _cancelAddNewGroup() {
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController();
|
||||
_focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (_focusNode.hasFocus &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
_cancelAddNewGroup();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
)..addListener(() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
_cancelAddNewGroup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// call after every setState
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isEditing) {
|
||||
_focusNode.requestFocus();
|
||||
widget.scrollController.jumpTo(
|
||||
widget.scrollController.position.maxScrollExtent,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: isEditing
|
||||
? SizedBox(
|
||||
width: 256,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 8.0),
|
||||
child: FlowyIconButton(
|
||||
icon: const FlowySvg(FlowySvgs.close_filled_m),
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => _textController.clear(),
|
||||
),
|
||||
),
|
||||
suffixIconConstraints:
|
||||
BoxConstraints.loose(const Size(20, 24)),
|
||||
border: const UnderlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
|
||||
isDense: true,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
onSubmitted: (groupName) => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createGroup(groupName)),
|
||||
),
|
||||
),
|
||||
)
|
||||
: FlowyTooltip(
|
||||
message: LocaleKeys.board_column_createNewColumn.tr(),
|
||||
child: FlowyIconButton(
|
||||
width: 26,
|
||||
icon: const FlowySvg(FlowySvgs.add_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () => setState(() {
|
||||
isEditing = true;
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "6aba8dd"
|
||||
resolved-ref: "6aba8ddd86839ca09b997cb2457f013236e0c337"
|
||||
ref: "1a329c2"
|
||||
resolved-ref: "1a329c21921c0d19871bea3237b7d80fe131f2ed"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
|
@ -43,7 +43,7 @@ dependencies:
|
||||
# path: packages/appflowy_board
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: 6aba8dd
|
||||
ref: 1a329c2
|
||||
appflowy_editor: ^1.5.1
|
||||
appflowy_popover:
|
||||
path: packages/appflowy_popover
|
||||
|
3
frontend/resources/flowy_icons/24x/close_filled.svg
Normal file
3
frontend/resources/flowy_icons/24x/close_filled.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM10.1392 5.14768C10.3361 4.95077 10.6554 4.95077 10.8523 5.14768C11.0492 5.34459 11.0492 5.66385 10.8523 5.86076L8.71303 8L10.8523 10.1392C11.0492 10.3362 11.0492 10.6554 10.8523 10.8523C10.6554 11.0492 10.3361 11.0492 10.1392 10.8523L7.99996 8.71307L5.86076 10.8523C5.66385 11.0492 5.34459 11.0492 5.14768 10.8523C4.95077 10.6554 4.95077 10.3361 5.14768 10.1392L7.28688 8L5.14769 5.8608C4.95078 5.66389 4.95078 5.34464 5.14769 5.14773C5.3446 4.95082 5.66385 4.95082 5.86076 5.14773L7.99996 7.28692L10.1392 5.14768Z" fill="#1F2329" fill-opacity="0.4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 809 B |
@ -755,7 +755,8 @@
|
||||
"board": {
|
||||
"column": {
|
||||
"createNewCard": "New",
|
||||
"renameGroupTooltip": "Press to rename group"
|
||||
"renameGroupTooltip": "Press to rename group",
|
||||
"createNewColumn": "Add a new group"
|
||||
},
|
||||
"menuName": "Board",
|
||||
"showUngrouped": "Show ungrouped items",
|
||||
|
@ -3,7 +3,7 @@ use event_integration::EventIntegrationTest;
|
||||
#[tokio::test]
|
||||
async fn update_group_name_test() {
|
||||
let test = EventIntegrationTest::new_with_guest_user().await;
|
||||
let current_workspace = test.get_current_workspace().await.workspace;
|
||||
let current_workspace = test.get_current_workspace().await;
|
||||
let board_view = test
|
||||
.create_board(¤t_workspace.id, "my board view".to_owned(), vec![])
|
||||
.await;
|
||||
|
@ -197,3 +197,36 @@ impl From<UpdateGroupParams> for GroupChangeset {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, ProtoBuf)]
|
||||
pub struct CreateGroupPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub group_config_id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateGroupParams {
|
||||
pub view_id: String,
|
||||
pub group_config_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl TryFrom<CreateGroupPayloadPB> for CreateGroupParams {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: CreateGroupPayloadPB) -> Result<Self, Self::Error> {
|
||||
let view_id = NotEmptyStr::parse(value.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
|
||||
let name = NotEmptyStr::parse(value.name).map_err(|_| ErrorCode::ViewIdIsInvalid)?;
|
||||
Ok(CreateGroupParams {
|
||||
view_id: view_id.0,
|
||||
group_config_id: value.group_config_id,
|
||||
name: name.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -741,6 +741,20 @@ pub(crate) async fn move_group_row_handler(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(manager), err)]
|
||||
pub(crate) async fn create_group_handler(
|
||||
data: AFPluginData<CreateGroupPayloadPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> FlowyResult<()> {
|
||||
let manager = upgrade_manager(manager)?;
|
||||
let params: CreateGroupParams = data.into_inner().try_into()?;
|
||||
let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?;
|
||||
database_editor
|
||||
.create_group(¶ms.view_id, ¶ms.name)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(manager), err)]
|
||||
pub(crate) async fn get_databases_handler(
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
|
@ -60,6 +60,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
|
||||
.event(DatabaseEvent::GetGroups, get_groups_handler)
|
||||
.event(DatabaseEvent::GetGroup, get_group_handler)
|
||||
.event(DatabaseEvent::UpdateGroup, update_group_handler)
|
||||
.event(DatabaseEvent::CreateGroup, create_group_handler)
|
||||
// Database
|
||||
.event(DatabaseEvent::GetDatabases, get_databases_handler)
|
||||
// Calendar
|
||||
@ -284,6 +285,9 @@ pub enum DatabaseEvent {
|
||||
#[event(input = "UpdateGroupPB")]
|
||||
UpdateGroup = 113,
|
||||
|
||||
#[event(input = "CreateGroupPayloadPB")]
|
||||
CreateGroup = 114,
|
||||
|
||||
/// Returns all the databases
|
||||
#[event(output = "RepeatedDatabaseDescriptionPB")]
|
||||
GetDatabases = 120,
|
||||
|
@ -442,7 +442,7 @@ impl DatabaseEditor {
|
||||
let row_detail = self.database.lock().get_row_detail(&row_order.id);
|
||||
if let Some(row_detail) = row_detail {
|
||||
for view in self.database_views.editors().await {
|
||||
view.v_did_create_row(&row_detail, &group_id, index).await;
|
||||
view.v_did_create_row(&row_detail, index).await;
|
||||
}
|
||||
return Ok(Some(row_detail));
|
||||
}
|
||||
@ -961,6 +961,12 @@ impl DatabaseEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_group(&self, view_id: &str, name: &str) -> FlowyResult<()> {
|
||||
let view_editor = self.database_views.get_view_editor(view_id).await?;
|
||||
view_editor.v_create_group(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn set_layout_setting(
|
||||
&self,
|
||||
|
@ -14,8 +14,8 @@ use lib_dispatch::prelude::af_spawn;
|
||||
use crate::entities::{
|
||||
CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams,
|
||||
DeleteGroupParams, DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB,
|
||||
GroupRowsNotificationPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB,
|
||||
RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams,
|
||||
InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB,
|
||||
SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams,
|
||||
};
|
||||
use crate::notification::{send_notification, DatabaseNotification};
|
||||
use crate::services::cell::CellCache;
|
||||
@ -126,39 +126,22 @@ impl DatabaseViewEditor {
|
||||
.send();
|
||||
}
|
||||
|
||||
pub async fn v_did_create_row(
|
||||
&self,
|
||||
row_detail: &RowDetail,
|
||||
group_id: &Option<String>,
|
||||
index: usize,
|
||||
) {
|
||||
let changes: RowsChangePB;
|
||||
pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) {
|
||||
// Send the group notification if the current view has groups
|
||||
match group_id.as_ref() {
|
||||
None => {
|
||||
let row = InsertedRowPB::new(RowMetaPB::from(row_detail)).with_index(index as i32);
|
||||
changes = RowsChangePB::from_insert(row);
|
||||
},
|
||||
Some(group_id) => {
|
||||
self
|
||||
.mut_group_controller(|group_controller, _| {
|
||||
group_controller.did_create_row(row_detail, group_id);
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Some(controller) = self.group_controller.write().await.as_mut() {
|
||||
let changesets = controller.did_create_row(row_detail, index);
|
||||
|
||||
let inserted_row = InsertedRowPB {
|
||||
row_meta: RowMetaPB::from(row_detail),
|
||||
index: Some(index as i32),
|
||||
is_new: true,
|
||||
};
|
||||
let changeset =
|
||||
GroupRowsNotificationPB::insert(group_id.clone(), vec![inserted_row.clone()]);
|
||||
for changeset in changesets {
|
||||
notify_did_update_group_rows(changeset).await;
|
||||
changes = RowsChangePB::from_insert(inserted_row);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@ -168,16 +151,22 @@ impl DatabaseViewEditor {
|
||||
pub async fn v_did_delete_row(&self, row: &Row) {
|
||||
// Send the group notification if the current view has groups;
|
||||
let result = self
|
||||
.mut_group_controller(|group_controller, field| {
|
||||
group_controller.did_delete_delete_row(row, &field)
|
||||
})
|
||||
.mut_group_controller(|group_controller, _| group_controller.did_delete_row(row))
|
||||
.await;
|
||||
|
||||
if let Some(result) = result {
|
||||
tracing::trace!("Delete row in view changeset: {:?}", result.row_changesets);
|
||||
tracing::trace!("Delete row in view changeset: {:?}", result);
|
||||
for changeset in result.row_changesets {
|
||||
notify_did_update_group_rows(changeset).await;
|
||||
}
|
||||
if let Some(deleted_group) = result.deleted_group {
|
||||
let payload = GroupChangesPB {
|
||||
view_id: self.view_id.clone(),
|
||||
deleted_groups: vec![deleted_group.group_id],
|
||||
..Default::default()
|
||||
};
|
||||
notify_did_update_num_of_groups(&self.view_id, payload).await;
|
||||
}
|
||||
}
|
||||
let changes = RowsChangePB::from_delete(row.id.clone().into_inner());
|
||||
send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows)
|
||||
@ -319,7 +308,7 @@ impl DatabaseViewEditor {
|
||||
.read()
|
||||
.await
|
||||
.as_ref()?
|
||||
.groups()
|
||||
.get_all_groups()
|
||||
.into_iter()
|
||||
.filter(|group| group.is_visible)
|
||||
.map(|group_data| GroupPB::from(group_data.clone()))
|
||||
@ -371,6 +360,36 @@ impl DatabaseViewEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn v_create_group(&self, name: &str) -> FlowyResult<()> {
|
||||
let mut old_field: Option<Field> = None;
|
||||
let result = if let Some(controller) = self.group_controller.write().await.as_mut() {
|
||||
let create_group_results = controller.create_group(name.to_string())?;
|
||||
old_field = self.delegate.get_field(controller.field_id());
|
||||
create_group_results
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if let Some(old_field) = old_field {
|
||||
if let (Some(type_option_data), Some(payload)) = result {
|
||||
self
|
||||
.delegate
|
||||
.update_field(&self.view_id, type_option_data, old_field)
|
||||
.await?;
|
||||
|
||||
let group_changes = GroupChangesPB {
|
||||
view_id: self.view_id.clone(),
|
||||
inserted_groups: vec![payload],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
notify_did_update_num_of_groups(&self.view_id, group_changes).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
@ -671,7 +690,7 @@ impl DatabaseViewEditor {
|
||||
.await?;
|
||||
|
||||
let new_groups = new_group_controller
|
||||
.groups()
|
||||
.get_all_groups()
|
||||
.into_iter()
|
||||
.map(|group| GroupPB::from(group.clone()))
|
||||
.collect();
|
||||
|
@ -25,15 +25,27 @@ pub trait SelectTypeOptionSharedAction: Send + Sync {
|
||||
/// If the option already exists, it will be updated.
|
||||
/// If the option does not exist, it will be inserted at the beginning.
|
||||
fn insert_option(&mut self, new_option: SelectOption) {
|
||||
self.insert_option_at_index(new_option, None);
|
||||
}
|
||||
|
||||
fn insert_option_at_index(&mut self, new_option: SelectOption, new_index: Option<usize>) {
|
||||
let options = self.mut_options();
|
||||
let safe_new_index = new_index.map(|index| {
|
||||
if index > options.len() {
|
||||
options.len()
|
||||
} else {
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(index) = options
|
||||
.iter()
|
||||
.position(|option| option.id == new_option.id || option.name == new_option.name)
|
||||
{
|
||||
options.remove(index);
|
||||
options.insert(index, new_option);
|
||||
options.insert(safe_new_index.unwrap_or(index), new_option);
|
||||
} else {
|
||||
options.insert(0, new_option);
|
||||
options.insert(safe_new_index.unwrap_or(0), new_option);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ pub trait GroupCustomize: Send + Sync {
|
||||
&mut self,
|
||||
row: &Row,
|
||||
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> Vec<GroupRowsNotificationPB>;
|
||||
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>);
|
||||
|
||||
/// Move row from one group to another
|
||||
fn move_row(
|
||||
@ -71,27 +71,71 @@ pub trait GroupCustomize: Send + Sync {
|
||||
) -> Option<GroupPB> {
|
||||
None
|
||||
}
|
||||
|
||||
fn generate_new_group(
|
||||
&mut self,
|
||||
_name: String,
|
||||
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
|
||||
Ok((None, None))
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the shared actions any group controller can perform.
|
||||
#[async_trait]
|
||||
pub trait GroupControllerOperation: Send + Sync {
|
||||
/// The field that is used for grouping the rows
|
||||
/// Returns the id of field that is being used to group the rows
|
||||
fn field_id(&self) -> &str;
|
||||
|
||||
/// Returns number of groups the current field has
|
||||
fn groups(&self) -> Vec<&GroupData>;
|
||||
/// Returns all of the groups currently managed by the controller
|
||||
fn get_all_groups(&self) -> Vec<&GroupData>;
|
||||
|
||||
/// Returns the index and the group data with group_id
|
||||
/// Returns the index and the group data with the given group id if it exists.
|
||||
///
|
||||
/// * `group_id` - A string slice that is used to match the group
|
||||
fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)>;
|
||||
|
||||
/// Separates the rows into different groups
|
||||
/// Sort the rows into the different groups.
|
||||
///
|
||||
/// * `rows`: rows to be inserted
|
||||
/// * `field`: reference to the field being sorted (currently unused)
|
||||
fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()>;
|
||||
|
||||
/// Remove the group with from_group_id and insert it to the index with to_group_id
|
||||
/// Create a new group, currently only supports single and multi-select.
|
||||
///
|
||||
/// Returns a new type option data for the grouping field if it's altered.
|
||||
///
|
||||
/// * `name`: name of the new group
|
||||
fn create_group(
|
||||
&mut self,
|
||||
name: String,
|
||||
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)>;
|
||||
|
||||
/// Reorders the group in the group controller.
|
||||
///
|
||||
/// * `from_group_id`: id of the group being moved
|
||||
/// * `to_group_id`: id of the group whose index is the one at which the
|
||||
/// reordered group will be placed
|
||||
fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>;
|
||||
|
||||
/// Insert/Remove the row to the group if the corresponding cell data is changed
|
||||
/// Adds a newly-created row to one or more suitable groups.
|
||||
///
|
||||
/// Returns a changeset payload to be sent as a notification.
|
||||
///
|
||||
/// * `row_detail`: the newly-created row
|
||||
fn did_create_row(
|
||||
&mut self,
|
||||
row_detail: &RowDetail,
|
||||
index: usize,
|
||||
) -> Vec<GroupRowsNotificationPB>;
|
||||
|
||||
/// Called after a row's cell data is changed, this moves the row to the
|
||||
/// correct group. It may also insert a new group and/or remove an old group.
|
||||
///
|
||||
/// Returns the inserted and removed groups if necessary for notification.
|
||||
///
|
||||
/// * `old_row_detail`:
|
||||
/// * `row_detail`:
|
||||
/// * `field`:
|
||||
fn did_update_group_row(
|
||||
&mut self,
|
||||
old_row_detail: &Option<RowDetail>,
|
||||
@ -99,22 +143,31 @@ pub trait GroupControllerOperation: Send + Sync {
|
||||
field: &Field,
|
||||
) -> FlowyResult<DidUpdateGroupRowResult>;
|
||||
|
||||
/// Remove the row from the group if the row gets deleted
|
||||
fn did_delete_delete_row(
|
||||
&mut self,
|
||||
row: &Row,
|
||||
field: &Field,
|
||||
) -> FlowyResult<DidMoveGroupRowResult>;
|
||||
/// Called after the row is deleted, this removes the row from the group.
|
||||
/// A group could be deleted as a result.
|
||||
///
|
||||
/// Returns a the removed group when this occurs.
|
||||
fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult>;
|
||||
|
||||
/// Move the row from one group to another group
|
||||
/// Reorders a row within the current group or move the row to another group.
|
||||
///
|
||||
/// * `context`: information about the row being moved and its destination
|
||||
fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult>;
|
||||
|
||||
/// Update the group if the corresponding field is changed
|
||||
/// Updates the groups after a field change. (currently never does anything)
|
||||
///
|
||||
/// * `field`: new changeset
|
||||
fn did_update_group_field(&mut self, field: &Field) -> FlowyResult<Option<GroupChangesPB>>;
|
||||
|
||||
/// Updates the name and/or visibility of groups.
|
||||
///
|
||||
/// Returns a non-empty `TypeOptionData` when the changes require a change
|
||||
/// in the field type option data.
|
||||
///
|
||||
/// * `changesets`: list of changesets to be made to one or more groups
|
||||
async fn apply_group_changeset(
|
||||
&mut self,
|
||||
changeset: &GroupChangesets,
|
||||
changesets: &GroupChangesets,
|
||||
) -> FlowyResult<TypeOptionData>;
|
||||
}
|
||||
|
||||
|
@ -124,6 +124,7 @@ where
|
||||
/// Returns the no `status` group
|
||||
///
|
||||
/// We take the `id` of the `field` as the no status group id
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> {
|
||||
self.group_by_id.get(&self.field.id)
|
||||
}
|
||||
@ -249,7 +250,7 @@ where
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `generated_group_configs`: the generated groups contains a list of [GeneratedGroupConfig].
|
||||
/// * `generated_groups`: the generated groups contains a list of [GeneratedGroupConfig].
|
||||
///
|
||||
/// Each [FieldType] can implement the [GroupGenerator] trait in order to generate different
|
||||
/// groups. For example, the FieldType::Checkbox has the [CheckboxGroupGenerator] that implements
|
||||
|
@ -11,7 +11,7 @@ use serde::Serialize;
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
use crate::entities::{
|
||||
FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB,
|
||||
FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB,
|
||||
};
|
||||
use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser};
|
||||
use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData};
|
||||
@ -38,9 +38,6 @@ pub trait GroupController: GroupControllerOperation + Send + Sync {
|
||||
|
||||
/// Called before the row was created.
|
||||
fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str);
|
||||
|
||||
/// Called after the row was created.
|
||||
fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -184,7 +181,7 @@ where
|
||||
&self.grouping_field_id
|
||||
}
|
||||
|
||||
fn groups(&self) -> Vec<&GroupData> {
|
||||
fn get_all_groups(&self) -> Vec<&GroupData> {
|
||||
self.context.groups()
|
||||
}
|
||||
|
||||
@ -233,10 +230,70 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_group(
|
||||
&mut self,
|
||||
name: String,
|
||||
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
|
||||
self.generate_new_group(name)
|
||||
}
|
||||
|
||||
fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> {
|
||||
self.context.move_group(from_group_id, to_group_id)
|
||||
}
|
||||
|
||||
fn did_create_row(
|
||||
&mut self,
|
||||
row_detail: &RowDetail,
|
||||
index: usize,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
let cell = match row_detail.row.cells.get(&self.grouping_field_id) {
|
||||
None => self.placeholder_cell(),
|
||||
Some(cell) => Some(cell.clone()),
|
||||
};
|
||||
|
||||
let mut changesets: Vec<GroupRowsNotificationPB> = vec![];
|
||||
if let Some(cell) = cell {
|
||||
let cell_data = <T as TypeOption>::CellData::from(&cell);
|
||||
|
||||
let mut suitable_group_ids = vec![];
|
||||
|
||||
for group in self.get_all_groups() {
|
||||
if self.can_group(&group.filter_content, &cell_data) {
|
||||
suitable_group_ids.push(group.id.clone());
|
||||
let changeset = GroupRowsNotificationPB::insert(
|
||||
group.id.clone(),
|
||||
vec![InsertedRowPB {
|
||||
row_meta: row_detail.into(),
|
||||
index: Some(index as i32),
|
||||
is_new: true,
|
||||
}],
|
||||
);
|
||||
changesets.push(changeset);
|
||||
}
|
||||
}
|
||||
if !suitable_group_ids.is_empty() {
|
||||
for group_id in suitable_group_ids.iter() {
|
||||
if let Some(group) = self.context.get_mut_group(group_id) {
|
||||
group.add_row(row_detail.clone());
|
||||
}
|
||||
}
|
||||
} else if let Some(no_status_group) = self.context.get_mut_no_status_group() {
|
||||
no_status_group.add_row(row_detail.clone());
|
||||
let changeset = GroupRowsNotificationPB::insert(
|
||||
no_status_group.id.clone(),
|
||||
vec![InsertedRowPB {
|
||||
row_meta: row_detail.into(),
|
||||
index: Some(index as i32),
|
||||
is_new: true,
|
||||
}],
|
||||
);
|
||||
changesets.push(changeset);
|
||||
}
|
||||
}
|
||||
|
||||
changesets
|
||||
}
|
||||
|
||||
fn did_update_group_row(
|
||||
&mut self,
|
||||
old_row_detail: &Option<RowDetail>,
|
||||
@ -278,26 +335,21 @@ where
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn did_delete_delete_row(
|
||||
&mut self,
|
||||
row: &Row,
|
||||
_field: &Field,
|
||||
) -> FlowyResult<DidMoveGroupRowResult> {
|
||||
// if the cell_rev is none, then the row must in the default group.
|
||||
fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult> {
|
||||
let mut result = DidMoveGroupRowResult {
|
||||
deleted_group: None,
|
||||
row_changesets: vec![],
|
||||
};
|
||||
// early return if the row is not in the default group
|
||||
if let Some(cell) = row.cells.get(&self.grouping_field_id) {
|
||||
let cell_data = <T as TypeOption>::CellData::from(cell);
|
||||
if !cell_data.is_cell_empty() {
|
||||
tracing::error!("did_delete_delete_row {:?}", cell);
|
||||
result.row_changesets = self.delete_row(row, &cell_data);
|
||||
(result.deleted_group, result.row_changesets) = self.delete_row(row, &cell_data);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
match self.context.get_no_status_group() {
|
||||
match self.context.get_mut_no_status_group() {
|
||||
None => {
|
||||
tracing::error!("Unexpected None value. It should have the no status group");
|
||||
},
|
||||
@ -305,6 +357,7 @@ where
|
||||
if !no_status_group.contains_row(&row.id) {
|
||||
tracing::error!("The row: {:?} should be in the no status group", row.id);
|
||||
}
|
||||
no_status_group.remove_row(&row.id);
|
||||
result.row_changesets = vec![GroupRowsNotificationPB::delete(
|
||||
no_status_group.id.clone(),
|
||||
vec![row.id.clone().into_inner()],
|
||||
|
@ -3,7 +3,7 @@ use collab_database::fields::{Field, TypeOptionData};
|
||||
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB};
|
||||
use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB};
|
||||
use crate::services::cell::insert_checkbox_cell;
|
||||
use crate::services::field::{
|
||||
CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK,
|
||||
@ -109,7 +109,7 @@ impl GroupCustomize for CheckboxGroupController {
|
||||
&mut self,
|
||||
row: &Row,
|
||||
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) {
|
||||
let mut changesets = vec![];
|
||||
self.context.iter_mut_groups(|group| {
|
||||
let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
|
||||
@ -122,7 +122,7 @@ impl GroupCustomize for CheckboxGroupController {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
});
|
||||
changesets
|
||||
(None, changesets)
|
||||
}
|
||||
|
||||
fn move_row(
|
||||
@ -155,12 +155,6 @@ impl GroupController for CheckboxGroupController {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) {
|
||||
if let Some(group) = self.context.get_mut_group(group_id) {
|
||||
group.add_row(row_detail.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckboxGroupBuilder();
|
||||
|
@ -178,8 +178,8 @@ impl GroupCustomize for DateGroupController {
|
||||
fn delete_row(
|
||||
&mut self,
|
||||
row: &Row,
|
||||
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) {
|
||||
let mut changesets = vec![];
|
||||
self.context.iter_mut_groups(|group| {
|
||||
let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
|
||||
@ -192,7 +192,23 @@ impl GroupCustomize for DateGroupController {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
});
|
||||
changesets
|
||||
|
||||
let setting_content = self.context.get_setting_content();
|
||||
let deleted_group =
|
||||
match self
|
||||
.context
|
||||
.get_group(&group_id(cell_data, &self.type_option, &setting_content))
|
||||
{
|
||||
Some((_, group)) if group.rows.len() == 1 => Some(group.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let deleted_group = deleted_group.map(|group| {
|
||||
let _ = self.context.delete_group(&group.id);
|
||||
group.into()
|
||||
});
|
||||
|
||||
(deleted_group, changesets)
|
||||
}
|
||||
|
||||
fn move_row(
|
||||
@ -247,12 +263,6 @@ impl GroupController for DateGroupController {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) {
|
||||
if let Some(group) = self.context.get_mut_group(group_id) {
|
||||
group.add_row(row_detail.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DateGroupBuilder();
|
||||
|
@ -6,7 +6,7 @@ use collab_database::rows::{Cells, Row, RowDetail};
|
||||
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
use crate::entities::GroupChangesPB;
|
||||
use crate::entities::{GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB};
|
||||
use crate::services::group::action::{
|
||||
DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation,
|
||||
};
|
||||
@ -44,7 +44,7 @@ impl GroupControllerOperation for DefaultGroupController {
|
||||
&self.field_id
|
||||
}
|
||||
|
||||
fn groups(&self) -> Vec<&GroupData> {
|
||||
fn get_all_groups(&self) -> Vec<&GroupData> {
|
||||
vec![&self.group]
|
||||
}
|
||||
|
||||
@ -59,10 +59,34 @@ impl GroupControllerOperation for DefaultGroupController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_group(
|
||||
&mut self,
|
||||
_name: String,
|
||||
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
|
||||
Ok((None, None))
|
||||
}
|
||||
|
||||
fn move_group(&mut self, _from_group_id: &str, _to_group_id: &str) -> FlowyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn did_create_row(
|
||||
&mut self,
|
||||
row_detail: &RowDetail,
|
||||
index: usize,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
self.group.add_row(row_detail.clone());
|
||||
|
||||
vec![GroupRowsNotificationPB::insert(
|
||||
self.group.id.clone(),
|
||||
vec![InsertedRowPB {
|
||||
row_meta: row_detail.into(),
|
||||
index: Some(index as i32),
|
||||
is_new: true,
|
||||
}],
|
||||
)]
|
||||
}
|
||||
|
||||
fn did_update_group_row(
|
||||
&mut self,
|
||||
_old_row_detail: &Option<RowDetail>,
|
||||
@ -76,14 +100,15 @@ impl GroupControllerOperation for DefaultGroupController {
|
||||
})
|
||||
}
|
||||
|
||||
fn did_delete_delete_row(
|
||||
&mut self,
|
||||
_row: &Row,
|
||||
_field: &Field,
|
||||
) -> FlowyResult<DidMoveGroupRowResult> {
|
||||
fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult> {
|
||||
let mut changeset = GroupRowsNotificationPB::new(self.group.id.clone());
|
||||
if self.group.contains_row(&row.id) {
|
||||
self.group.remove_row(&row.id);
|
||||
changeset.deleted_rows.push(row.id.clone().into_inner());
|
||||
}
|
||||
Ok(DidMoveGroupRowResult {
|
||||
deleted_group: None,
|
||||
row_changesets: vec![],
|
||||
row_changesets: vec![changeset],
|
||||
})
|
||||
}
|
||||
|
||||
@ -115,6 +140,4 @@ impl GroupController for DefaultGroupController {
|
||||
}
|
||||
|
||||
fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {}
|
||||
|
||||
fn did_create_row(&mut self, _row_detail: &RowDetail, _group_id: &str) {}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use async_trait::async_trait;
|
||||
use collab_database::fields::{Field, TypeOptionData};
|
||||
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
|
||||
use flowy_error::FlowyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{FieldType, GroupRowsNotificationPB};
|
||||
use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB};
|
||||
use crate::services::cell::insert_select_option_cell;
|
||||
use crate::services::field::{
|
||||
MultiSelectTypeOption, SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction,
|
||||
@ -13,7 +14,7 @@ use crate::services::group::action::GroupCustomize;
|
||||
use crate::services::group::controller::{BaseGroupController, GroupController};
|
||||
use crate::services::group::{
|
||||
add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group,
|
||||
move_group_row, remove_select_option_row, GeneratedGroups, GroupChangeset, GroupContext,
|
||||
move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset, GroupContext,
|
||||
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
|
||||
};
|
||||
|
||||
@ -69,14 +70,14 @@ impl GroupCustomize for MultiSelectGroupController {
|
||||
&mut self,
|
||||
row: &Row,
|
||||
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) {
|
||||
let mut changesets = vec![];
|
||||
self.context.iter_mut_status_groups(|group| {
|
||||
if let Some(changeset) = remove_select_option_row(group, cell_data, row) {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
});
|
||||
changesets
|
||||
(None, changesets)
|
||||
}
|
||||
|
||||
fn move_row(
|
||||
@ -92,6 +93,20 @@ impl GroupCustomize for MultiSelectGroupController {
|
||||
});
|
||||
group_changeset
|
||||
}
|
||||
|
||||
fn generate_new_group(
|
||||
&mut self,
|
||||
name: String,
|
||||
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
|
||||
let mut new_type_option = self.type_option.clone();
|
||||
let new_select_option = self.type_option.create_option(&name);
|
||||
new_type_option.insert_option(new_select_option.clone());
|
||||
|
||||
let new_group = Group::new(new_select_option.id, new_select_option.name);
|
||||
let inserted_group_pb = self.context.add_new_group(new_group)?;
|
||||
|
||||
Ok((Some(new_type_option.into()), Some(inserted_group_pb)))
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupController for MultiSelectGroupController {
|
||||
@ -106,12 +121,6 @@ impl GroupController for MultiSelectGroupController {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) {
|
||||
if let Some(group) = self.context.get_mut_group(group_id) {
|
||||
group.add_row(row_detail.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MultiSelectGroupBuilder;
|
||||
|
@ -1,9 +1,10 @@
|
||||
use async_trait::async_trait;
|
||||
use collab_database::fields::{Field, TypeOptionData};
|
||||
use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail};
|
||||
use flowy_error::FlowyResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{FieldType, GroupRowsNotificationPB};
|
||||
use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB};
|
||||
use crate::services::cell::insert_select_option_cell;
|
||||
use crate::services::field::{
|
||||
SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, SingleSelectTypeOption,
|
||||
@ -14,8 +15,8 @@ use crate::services::group::controller::{BaseGroupController, GroupController};
|
||||
use crate::services::group::controller_impls::select_option_controller::util::*;
|
||||
use crate::services::group::entities::GroupData;
|
||||
use crate::services::group::{
|
||||
make_no_status_group, GeneratedGroups, GroupChangeset, GroupContext, GroupOperationInterceptor,
|
||||
GroupsBuilder, MoveGroupRowContext,
|
||||
make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupContext,
|
||||
GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext,
|
||||
};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
@ -70,14 +71,14 @@ impl GroupCustomize for SingleSelectGroupController {
|
||||
&mut self,
|
||||
row: &Row,
|
||||
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) {
|
||||
let mut changesets = vec![];
|
||||
self.context.iter_mut_status_groups(|group| {
|
||||
if let Some(changeset) = remove_select_option_row(group, cell_data, row) {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
});
|
||||
changesets
|
||||
(None, changesets)
|
||||
}
|
||||
|
||||
fn move_row(
|
||||
@ -93,6 +94,23 @@ impl GroupCustomize for SingleSelectGroupController {
|
||||
});
|
||||
group_changeset
|
||||
}
|
||||
|
||||
fn generate_new_group(
|
||||
&mut self,
|
||||
name: String,
|
||||
) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> {
|
||||
let mut new_type_option = self.type_option.clone();
|
||||
let new_select_option = self.type_option.create_option(&name);
|
||||
new_type_option.insert_option_at_index(
|
||||
new_select_option.clone(),
|
||||
Some(new_type_option.options.len()),
|
||||
);
|
||||
|
||||
let new_group = Group::new(new_select_option.id, new_select_option.name);
|
||||
let inserted_group_pb = self.context.add_new_group(new_group)?;
|
||||
|
||||
Ok((Some(new_type_option.into()), Some(inserted_group_pb)))
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupController for SingleSelectGroupController {
|
||||
@ -108,12 +126,6 @@ impl GroupController for SingleSelectGroupController {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) {
|
||||
if let Some(group) = self.context.get_mut_group(group_id) {
|
||||
group.add_row(row_detail.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SingleSelectGroupBuilder();
|
||||
|
@ -185,6 +185,7 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option<Cell> {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_select_option_groups(
|
||||
_field_id: &str,
|
||||
options: &[SelectOption],
|
||||
|
@ -128,8 +128,8 @@ impl GroupCustomize for URLGroupController {
|
||||
fn delete_row(
|
||||
&mut self,
|
||||
row: &Row,
|
||||
_cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> Vec<GroupRowsNotificationPB> {
|
||||
cell_data: &<Self::GroupTypeOption as TypeOption>::CellData,
|
||||
) -> (Option<GroupPB>, Vec<GroupRowsNotificationPB>) {
|
||||
let mut changesets = vec![];
|
||||
self.context.iter_mut_groups(|group| {
|
||||
let mut changeset = GroupRowsNotificationPB::new(group.id.clone());
|
||||
@ -142,7 +142,18 @@ impl GroupCustomize for URLGroupController {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
});
|
||||
changesets
|
||||
|
||||
let deleted_group = match self.context.get_group(&cell_data.data) {
|
||||
Some((_, group)) if group.rows.len() == 1 => Some(group.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let deleted_group = deleted_group.map(|group| {
|
||||
let _ = self.context.delete_group(&group.id);
|
||||
group.into()
|
||||
});
|
||||
|
||||
(deleted_group, changesets)
|
||||
}
|
||||
|
||||
fn move_row(
|
||||
@ -190,12 +201,6 @@ impl GroupController for URLGroupController {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) {
|
||||
if let Some(group) = self.context.get_mut_group(group_id) {
|
||||
group.add_row(row_detail.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct URLGroupGenerator();
|
||||
|
@ -67,6 +67,9 @@ pub enum GroupScript {
|
||||
group_id: String,
|
||||
group_name: String,
|
||||
},
|
||||
CreateGroup {
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct DatabaseGroupTest {
|
||||
@ -269,6 +272,11 @@ impl DatabaseGroupTest {
|
||||
assert_eq!(group_id, group.group_id, "group index: {}", group_index);
|
||||
assert_eq!(group_name, group.group_name, "group index: {}", group_index);
|
||||
},
|
||||
GroupScript::CreateGroup { name } => self
|
||||
.editor
|
||||
.create_group(&self.view_id, &name)
|
||||
.await
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -486,3 +486,19 @@ async fn group_group_by_other_field() {
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn group_manual_create_new_group() {
|
||||
let mut test = DatabaseGroupTest::new().await;
|
||||
let new_group_name = "Resumed";
|
||||
let scripts = vec![
|
||||
AssertGroupCount(4),
|
||||
CreateGroup {
|
||||
name: new_group_name.to_string(),
|
||||
},
|
||||
AssertGroupCount(5),
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
let new_group = test.group_at_index(4).await;
|
||||
assert_eq!(new_group.group_name, new_group_name);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user