diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index 78d6ab0090..ff1d5139c6 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -236,6 +236,7 @@ } }, "grid": { + "deleteView": "Are you sure you want to delete this view?", "settings": { "filter": "Filter", "sort": "Sort", diff --git a/frontend/appflowy_flutter/integration_test/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart new file mode 100644 index 0000000000..c3ef83c267 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart @@ -0,0 +1,78 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('update calendar layout', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateCalendarButton(); + + // open setting + await tester.tapDatabaseSettingButton(); + await tester.tapDatabaseLayoutButton(); + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); + + await tester.tapDatabaseSettingButton(); + await tester.tapDatabaseLayoutButton(); + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid); + + await tester.pumpAndSettle(); + }); + + testWidgets('calendar start from day setting', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create calendar view + await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar'); + + // Open setting + await tester.tapDatabaseSettingButton(); + await tester.tapCalendarLayoutSettingButton(); + + // select the first day of week is Monday + await tester.tapFirstDayOfWeek(); + await tester.tapFirstDayOfWeekStartFromMonday(); + + // Open the other page and open the new calendar page again + await tester.openPage(readme); + await tester.pumpAndSettle(const Duration(milliseconds: 300)); + await tester.openPage('calendar'); + + // Open setting again and check the start from Monday is selected + await tester.tapDatabaseSettingButton(); + await tester.tapCalendarLayoutSettingButton(); + await tester.tapFirstDayOfWeek(); + tester.assertFirstDayOfWeekStartFromMonday(); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_view_test.dart b/frontend/appflowy_flutter/integration_test/database_view_test.dart new file mode 100644 index 0000000000..7d787f2fef --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_view_test.dart @@ -0,0 +1,96 @@ +import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database', () { + const location = 'appflowy'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('create linked view', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Create board view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); + + // Create grid view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.grid); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid); + + // Create calendar view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.calendar); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar); + + await tester.pumpAndSettle(); + }); + + testWidgets('rename and delete linked view', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Create board view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); + + // rename board view + await tester.renameLinkedView( + tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board), + 'new board', + ); + final findBoard = tester.findTabBarLinkViewByViewName('new board'); + expect(findBoard, findsOneWidget); + + // delete the board + await tester.deleteDatebaseView(findBoard); + expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('delete the last database view', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Create board view + await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); + tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board); + + // delete the board + await tester.deleteDatebaseView( + tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board), + ); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index 12bb3f7163..5fc3162064 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -13,6 +13,8 @@ import 'database_row_page_test.dart' as database_row_page_test; import 'database_row_test.dart' as database_row_test; import 'database_setting_test.dart' as database_setting_test; import 'database_filter_test.dart' as database_filter_test; +import 'database_view_test.dart' as database_view_test; +import 'database_calendar_test.dart' as database_calendar_test; /// The main task runner for all integration tests in AppFlowy. /// @@ -29,6 +31,8 @@ void main() { share_markdown_test.main(); import_files_test.main(); document_with_database_test.main(); + + // Database integration tests database_cell_test.main(); database_field_test.main(); database_share_test.main(); @@ -36,6 +40,9 @@ void main() { database_row_test.main(); database_setting_test.main(); database_filter_test.main(); + database_view_test.main(); + database_calendar_test.main(); + // board_test.main(); // empty_document_test.main(); // smart_menu_test.main(); diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index ad9d79d08f..c553ad06ee 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -87,6 +87,7 @@ extension AppFlowyTestBase on WidgetTester { }) async { await tap( finder, + buttons: buttons, warnIfMissed: warnIfMissed, ); await pumpAndSettle(Duration(milliseconds: milliseconds)); diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 0ef1ecd8f6..7a86acd9f8 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -47,6 +47,13 @@ extension CommonOperations on WidgetTester { await tapButtonWithName(LocaleKeys.grid_menuName.tr()); } + /// Tap the create grid button. + /// + /// Must call [tapAddButton] first. + Future tapCreateCalendarButton() async { + await tapButtonWithName(LocaleKeys.calendar_menuName.tr()); + } + /// Tap the import button. /// /// Must call [tapAddButton] first. @@ -142,7 +149,9 @@ extension CommonOperations on WidgetTester { /// open the page with given name. Future openPage(String name) async { - await tapButton(findPageName(name)); + final finder = findPageName(name); + expect(finder, findsOneWidget); + await tapButton(finder); } /// Tap the ... button beside the page name. diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 2767ee1ad2..b8b71cdf42 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -1,10 +1,9 @@ import 'dart:io'; -import 'dart:ui'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; @@ -19,6 +18,9 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/filter_button.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; @@ -29,10 +31,13 @@ import 'package:appflowy/plugins/database_view/widgets/setting/database_setting. import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -728,6 +733,152 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(button); } + Future tapCalendarLayoutSettingButton() async { + final findSettingItem = find.byType(DatabaseSettingItem); + final findLayoutButton = find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.text == DatabaseSettingAction.showCalendarLayout.title(), + ); + + final button = find.descendant( + of: findSettingItem, + matching: findLayoutButton, + ); + + await tapButton(button); + } + + Future tapFirstDayOfWeek() async { + await tapButton(find.byType(FirstDayOfWeek)); + } + + Future tapFirstDayOfWeekStartFromSunday() async { + final finder = find.byWidgetPredicate( + (widget) => widget is StartFromButton && widget.dayIndex == 0, + ); + await tapButton(finder); + } + + Future tapFirstDayOfWeekStartFromMonday() async { + final finder = find.byWidgetPredicate( + (widget) => widget is StartFromButton && widget.dayIndex == 1, + ); + await tapButton(finder); + + // Dismiss the popover overlay in cause of obscure the tapButton + // in the next test case. + await sendKeyEvent(LogicalKeyboardKey.escape); + await pumpAndSettle(const Duration(milliseconds: 200)); + } + + void assertFirstDayOfWeekStartFromMonday() { + final finder = find.byWidgetPredicate( + (widget) => + widget is StartFromButton && + widget.dayIndex == 1 && + widget.isSelected == true, + ); + expect(finder, findsOneWidget); + } + + void assertFirstDayOfWeekStartFromSunday() { + final finder = find.byWidgetPredicate( + (widget) => + widget is StartFromButton && + widget.dayIndex == 0 && + widget.isSelected == true, + ); + expect(finder, findsOneWidget); + } + + Future tapCreateLinkedDatabaseViewButton(AddButtonAction action) async { + final findAddButton = find.byType(AddDatabaseViewButton); + await tapButton(findAddButton); + + final findCreateButton = find.byWidgetPredicate( + (widget) => + widget is TarBarAddButtonActionCell && widget.action == action, + ); + await tapButton(findCreateButton); + } + + Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) { + return find.byWidgetPredicate( + (widget) => widget is TabBarItemButton && widget.view.layout == layout, + ); + } + + Finder findTabBarLinkViewByViewName(String name) { + return find.byWidgetPredicate( + (widget) => widget is TabBarItemButton && widget.view.name == name, + ); + } + + Future renameLinkedView(Finder linkedView, String name) async { + await tap(linkedView, buttons: kSecondaryButton); + await pumpAndSettle(); + + await tapButton( + find.byWidgetPredicate( + (widget) => + widget is ActionCellWidget && + widget.action == TabBarViewAction.rename, + ), + ); + + await enterText( + find.descendant( + of: find.byType(FlowyFormTextInput), + matching: find.byType(TextFormField), + ), + name, + ); + + final field = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(field); + } + + Future deleteDatebaseView(Finder linkedView) async { + await tap(linkedView, buttons: kSecondaryButton); + await pumpAndSettle(); + + await tapButton( + find.byWidgetPredicate( + (widget) => + widget is ActionCellWidget && + widget.action == TabBarViewAction.delete, + ), + ); + + final okButton = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(okButton); + } + + Future assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) async { + switch (layout) { + case DatabaseLayoutPB.Board: + expect(find.byType(BoardPage), findsOneWidget); + break; + case DatabaseLayoutPB.Calendar: + expect(find.byType(CalendarPage), findsOneWidget); + break; + case DatabaseLayoutPB.Grid: + expect(find.byType(GridPage), findsOneWidget); + break; + default: + throw Exception('Unknown database layout type: $layout'); + } + } + Future selectDatabaseLayoutType(DatabaseLayoutPB layout) async { final findLayoutCell = find.byType(DatabaseViewLayoutCell); final findText = find.byWidgetPredicate( diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index 7c22494631..1f6c329d66 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -82,6 +82,7 @@ extension Expectation on WidgetTester { Finder findPageName(String name) { return find.byWidgetPredicate( (widget) => widget is ViewSectionItem && widget.view.name == name, + skipOffstage: false, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 89831ac15c..218d054815 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart'; import 'package:appflowy/plugins/database_view/application/view/view_cache.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; @@ -16,6 +15,7 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; import 'database_view_service.dart'; import 'defines.dart'; +import 'layout/layout_service.dart'; import 'layout/layout_setting_listener.dart'; import 'row/row_cache.dart'; import 'group/group_listener.dart'; @@ -50,16 +50,11 @@ class DatabaseLayoutSettingCallbacks { }); } -class CalendarLayoutCallbacks { - final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged; - - CalendarLayoutCallbacks({required this.onCalendarLayoutChanged}); -} - class DatabaseCallbacks { OnDatabaseChanged? onDatabaseChanged; OnFieldsChanged? onFieldsChanged; OnFiltersChanged? onFiltersChanged; + OnSortsChanged? onSortsChanged; OnNumOfRowsChanged? onNumOfRowsChanged; OnRowsDeleted? onRowsDeleted; OnRowsUpdated? onRowsUpdated; @@ -70,6 +65,7 @@ class DatabaseCallbacks { this.onNumOfRowsChanged, this.onFieldsChanged, this.onFiltersChanged, + this.onSortsChanged, this.onRowsUpdated, this.onRowsDeleted, this.onRowsCreated, @@ -80,15 +76,14 @@ class DatabaseController { final String viewId; final DatabaseViewBackendService _databaseViewBackendSvc; final FieldController fieldController; - DatabaseLayoutPB? databaseLayout; + DatabaseLayoutPB databaseLayout; DatabaseLayoutSettingPB? databaseLayoutSetting; late DatabaseViewCache _viewCache; // Callbacks - DatabaseCallbacks? _databaseCallbacks; - GroupCallbacks? _groupCallbacks; - DatabaseLayoutSettingCallbacks? _layoutCallbacks; - CalendarLayoutCallbacks? _calendarLayoutCallbacks; + final List _databaseCallbacks = []; + final List _groupCallbacks = []; + final List _layoutCallbacks = []; // Getters RowCache get rowCache => _viewCache.rowCache; @@ -96,15 +91,14 @@ class DatabaseController { // Listener final DatabaseGroupListener _groupListener; final DatabaseLayoutSettingListener _layoutListener; - final DatabaseCalendarLayoutListener _calendarLayoutListener; DatabaseController({required ViewPB view}) : viewId = view.id, _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id), fieldController = FieldController(viewId: view.id), _groupListener = DatabaseGroupListener(view.id), - _layoutListener = DatabaseLayoutSettingListener(view.id), - _calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) { + databaseLayout = databaseLayoutFromViewLayout(view.layout), + _layoutListener = DatabaseLayoutSettingListener(view.id) { _viewCache = DatabaseViewCache( viewId: viewId, fieldController: fieldController, @@ -115,29 +109,30 @@ class DatabaseController { _listenOnLayoutChanged(); } - void setListener({ + void addListener({ DatabaseCallbacks? onDatabaseChanged, DatabaseLayoutSettingCallbacks? onLayoutChanged, GroupCallbacks? onGroupChanged, - CalendarLayoutCallbacks? onCalendarLayoutChanged, }) { - _layoutCallbacks = onLayoutChanged; - _databaseCallbacks = onDatabaseChanged; - _groupCallbacks = onGroupChanged; - _calendarLayoutCallbacks = onCalendarLayoutChanged; + if (onLayoutChanged != null) { + _layoutCallbacks.add(onLayoutChanged); + } + + if (onDatabaseChanged != null) { + _databaseCallbacks.add(onDatabaseChanged); + } + + if (onGroupChanged != null) { + _groupCallbacks.add(onGroupChanged); + } } Future> open() async { - return _databaseViewBackendSvc.openGrid().then((result) { + return _databaseViewBackendSvc.openDatabase().then((result) { return result.fold( (DatabasePB database) async { databaseLayout = database.layoutType; - // Listen on layout changed if database layout is calendar - if (databaseLayout == DatabaseLayoutPB.Calendar) { - _listenOnCalendarLayoutChanged(); - } - // Load the actual database field data. final fieldsOrFail = await fieldController.loadFields( fieldIds: database.fields, @@ -146,7 +141,9 @@ class DatabaseController { (fields) { // Notify the database is changed after the fields are loaded. // The database won't can't be used until the fields are loaded. - _databaseCallbacks?.onDatabaseChanged?.call(database); + for (final callback in _databaseCallbacks) { + callback.onDatabaseChanged?.call(database); + } _viewCache.rowCache.setInitialRows(database.rows); return Future(() async { await _loadGroups(); @@ -217,11 +214,14 @@ class DatabaseController { ); } - Future updateCalenderLayoutSetting( - CalendarLayoutSettingPB layoutSetting, + Future updateLayoutSetting( + CalendarLayoutSettingPB calendarlLayoutSetting, ) async { await _databaseViewBackendSvc - .updateLayoutSetting(calendarLayoutSetting: layoutSetting) + .updateLayoutSetting( + calendarLayoutSetting: calendarlLayoutSetting, + layoutType: databaseLayout, + ) .then((result) { result.fold((l) => null, (r) => Log.error(r)); }); @@ -232,10 +232,9 @@ class DatabaseController { await fieldController.dispose(); await _groupListener.stop(); await _viewCache.dispose(); - _databaseCallbacks = null; - _groupCallbacks = null; - _layoutCallbacks = null; - _calendarLayoutCallbacks = null; + _databaseCallbacks.clear(); + _groupCallbacks.clear(); + _layoutCallbacks.clear(); } Future _loadGroups() async { @@ -243,7 +242,9 @@ class DatabaseController { return Future( () => result.fold( (groups) { - _groupCallbacks?.onGroupByField?.call(groups.items); + for (final callback in _groupCallbacks) { + callback.onGroupByField?.call(groups.items); + } }, (err) => Log.error(err), ), @@ -251,46 +252,63 @@ class DatabaseController { } Future _loadLayoutSetting() async { - if (databaseLayout != null) { - _databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) { - result.fold( - (newDatabaseLayoutSetting) { - databaseLayoutSetting = newDatabaseLayoutSetting; - databaseLayoutSetting?.freeze(); + _databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) { + result.fold( + (newDatabaseLayoutSetting) { + databaseLayoutSetting = newDatabaseLayoutSetting; + databaseLayoutSetting?.freeze(); - _layoutCallbacks?.onLoadLayout(newDatabaseLayoutSetting); - }, - (r) => Log.error(r), - ); - }); - } + for (final callback in _layoutCallbacks) { + callback.onLoadLayout(newDatabaseLayoutSetting); + } + }, + (r) => Log.error(r), + ); + }); } void _listenOnRowsChanged() { final callbacks = DatabaseViewCallbacks( onNumOfRowsChanged: (rows, rowByRowId, reason) { - _databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason); + for (final callback in _databaseCallbacks) { + callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason); + } }, onRowsDeleted: (ids) { - _databaseCallbacks?.onRowsDeleted?.call(ids); + for (final callback in _databaseCallbacks) { + callback.onRowsDeleted?.call(ids); + } }, onRowsUpdated: (ids, reason) { - _databaseCallbacks?.onRowsUpdated?.call(ids, reason); + for (final callback in _databaseCallbacks) { + callback.onRowsUpdated?.call(ids, reason); + } }, onRowsCreated: (ids) { - _databaseCallbacks?.onRowsCreated?.call(ids); + for (final callback in _databaseCallbacks) { + callback.onRowsCreated?.call(ids); + } }, ); - _viewCache.setListener(callbacks); + _viewCache.addListener(callbacks); } void _listenOnFieldsChanged() { fieldController.addListener( onReceiveFields: (fields) { - _databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields)); + for (final callback in _databaseCallbacks) { + callback.onFieldsChanged?.call(UnmodifiableListView(fields)); + } + }, + onSorts: (sorts) { + for (final callback in _databaseCallbacks) { + callback.onSortsChanged?.call(sorts); + } }, onFilters: (filters) { - _databaseCallbacks?.onFiltersChanged?.call(filters); + for (final callback in _databaseCallbacks) { + callback.onFiltersChanged?.call(filters); + } }, ); } @@ -301,15 +319,21 @@ class DatabaseController { result.fold( (changeset) { if (changeset.updateGroups.isNotEmpty) { - _groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups); + for (final callback in _groupCallbacks) { + callback.onUpdateGroup?.call(changeset.updateGroups); + } } if (changeset.deletedGroups.isNotEmpty) { - _groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups); + for (final callback in _groupCallbacks) { + callback.onDeleteGroup?.call(changeset.deletedGroups); + } } for (final insertedGroup in changeset.insertedGroups) { - _groupCallbacks?.onInsertGroup?.call(insertedGroup); + for (final callback in _groupCallbacks) { + callback.onInsertGroup?.call(insertedGroup); + } } }, (r) => Log.error(r), @@ -318,7 +342,9 @@ class DatabaseController { onGroupByNewField: (result) { result.fold( (groups) { - _groupCallbacks?.onGroupByField?.call(groups); + for (final callback in _groupCallbacks) { + callback.onGroupByField?.call(groups); + } }, (r) => Log.error(r), ); @@ -330,24 +356,13 @@ class DatabaseController { _layoutListener.start( onLayoutChanged: (result) { result.fold( - (newDatabaseLayoutSetting) { - databaseLayoutSetting = newDatabaseLayoutSetting; + (newLayout) { + databaseLayoutSetting = newLayout; databaseLayoutSetting?.freeze(); - _layoutCallbacks?.onLayoutChanged(newDatabaseLayoutSetting); - }, - (r) => Log.error(r), - ); - }, - ); - } - - void _listenOnCalendarLayoutChanged() { - _calendarLayoutListener.start( - onCalendarLayoutChanged: (result) { - result.fold( - (l) { - _calendarLayoutCallbacks?.onCalendarLayoutChanged(l); + for (final callback in _layoutCallbacks) { + callback.onLayoutChanged(newLayout); + } }, (r) => Log.error(r), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index 64f9a2f1b6..623f99b94d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -25,7 +25,7 @@ class DatabaseViewBackendService { .then((value) => value.leftMap((l) => l.value)); } - Future> openGrid() async { + Future> openDatabase() async { final payload = DatabaseViewIdPB(value: viewId); return DatabaseEventGetDatabase(payload).send(); } @@ -113,9 +113,12 @@ class DatabaseViewBackendService { } Future> updateLayoutSetting({ + required DatabaseLayoutPB layoutType, CalendarLayoutSettingPB? calendarLayoutSetting, }) { - final payload = LayoutSettingChangesetPB.create()..viewId = viewId; + final payload = LayoutSettingChangesetPB.create() + ..viewId = viewId + ..layoutType = layoutType; if (calendarLayoutSetting != null) { payload.calendar = calendarLayoutSetting; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index 59ae345e2b..4ec8fbc0dc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -10,6 +11,7 @@ import 'row/row_service.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnFiltersChanged = void Function(List); +typedef OnSortsChanged = void Function(List); typedef OnDatabaseChanged = void Function(DatabasePB); typedef OnRowsCreated = void Function(List ids); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart index dd4f36fc09..5d517945fb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_service.dart @@ -15,13 +15,13 @@ class DatabaseLayoutBackendService { }) { final payload = UpdateViewPayloadPB.create() ..viewId = viewId - ..layout = _viewLayoutFromDatabaseLayout(layout); + ..layout = viewLayoutFromDatabaseLayout(layout); return FolderEventUpdateView(payload).send(); } } -ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { +ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { switch (databaseLayout) { case DatabaseLayoutPB.Board: return ViewLayoutPB.Board; @@ -33,3 +33,16 @@ ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { throw UnimplementedError; } } + +DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) { + switch (viewLayout) { + case ViewLayoutPB.Board: + return DatabaseLayoutPB.Board; + case ViewLayoutPB.Calendar: + return DatabaseLayoutPB.Calendar; + case ViewLayoutPB.Grid: + return DatabaseLayoutPB.Grid; + default: + throw UnimplementedError; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart deleted file mode 100644 index 1e128f8c75..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_bloc.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:dartz/dartz.dart'; - -part 'setting_bloc.freezed.dart'; - -class DatabaseSettingBloc - extends Bloc { - final String viewId; - DatabaseSettingBloc({required this.viewId}) - : super(DatabaseSettingState.initial()) { - on( - (event, emit) async { - event.map( - performAction: (_PerformAction value) { - emit(state.copyWith(selectedAction: Some(value.action))); - }, - ); - }, - ); - } -} - -@freezed -class DatabaseSettingEvent with _$DatabaseSettingEvent { - const factory DatabaseSettingEvent.performAction( - DatabaseSettingAction action, - ) = _PerformAction; -} - -@freezed -class DatabaseSettingState with _$DatabaseSettingState { - const factory DatabaseSettingState({ - required Option selectedAction, - }) = _DatabaseSettingState; - - factory DatabaseSettingState.initial() => DatabaseSettingState( - selectedAction: none(), - ); -} - -enum DatabaseSettingAction { - showProperties, - showLayout, - showGroup, - showCalendarLayout, -} - -extension DatabaseSettingActionExtension on DatabaseSettingAction { - String iconName() { - switch (this) { - case DatabaseSettingAction.showProperties: - return 'grid/setting/properties'; - case DatabaseSettingAction.showLayout: - return 'grid/setting/database_layout'; - case DatabaseSettingAction.showGroup: - return 'grid/setting/group'; - case DatabaseSettingAction.showCalendarLayout: - return 'grid/setting/calendar_layout'; - } - } - - String title() { - switch (this) { - case DatabaseSettingAction.showProperties: - return LocaleKeys.grid_settings_Properties.tr(); - case DatabaseSettingAction.showLayout: - return LocaleKeys.grid_settings_databaseLayout.tr(); - case DatabaseSettingAction.showGroup: - return LocaleKeys.grid_settings_group.tr(); - case DatabaseSettingAction.showCalendarLayout: - return LocaleKeys.calendar_settings_name.tr(); - } - } -} - -/// Returns the list of actions that should be shown for the given database layout. -List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { - switch (layout) { - case DatabaseLayoutPB.Board: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - DatabaseSettingAction.showGroup, - ]; - case DatabaseLayoutPB.Calendar: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - DatabaseSettingAction.showCalendarLayout, - ]; - case DatabaseLayoutPB.Grid: - return [ - DatabaseSettingAction.showProperties, - DatabaseSettingAction.showLayout, - ]; - default: - return []; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart new file mode 100644 index 0000000000..4b4697add2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/tar_bar_bloc.dart @@ -0,0 +1,290 @@ +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'database_controller.dart'; +import 'database_view_service.dart'; + +part 'tar_bar_bloc.freezed.dart'; + +class GridTabBarBloc extends Bloc { + GridTabBarBloc({ + bool isInlineView = false, + required ViewPB view, + }) : super(GridTabBarState.initial(view)) { + on( + (event, emit) async { + event.when( + initial: () { + _listenInlineViewChanged(); + _loadChildView(); + }, + didLoadChildViews: (List childViews) { + emit( + state.copyWith( + tabBars: [ + ...state.tabBars, + ...childViews.map( + (newChildView) => TarBar(view: newChildView), + ), + ], + tabBarControllerByViewId: _extendsTabBarController(childViews), + ), + ); + }, + selectView: (String viewId) { + final index = + state.tabBars.indexWhere((element) => element.viewId == viewId); + if (index != -1) { + emit( + state.copyWith(selectedIndex: index), + ); + } + }, + createView: (action) { + _createLinkedView(action.name, action.layoutType); + }, + deleteView: (String viewId) async { + final result = await ViewBackendService.delete(viewId: viewId); + result.fold( + (l) {}, + (r) => Log.error(r), + ); + }, + renameView: (String viewId, String newName) { + ViewBackendService.updateView(viewId: viewId, name: newName); + }, + didUpdateChildViews: (updatePB) async { + if (updatePB.createChildViews.isNotEmpty) { + final allTabBars = [ + ...state.tabBars, + ...updatePB.createChildViews.map((e) => TarBar(view: e)) + ]; + emit( + state.copyWith( + tabBars: allTabBars, + selectedIndex: state.tabBars.length, + tabBarControllerByViewId: + _extendsTabBarController(updatePB.createChildViews), + ), + ); + } + + if (updatePB.deleteChildViews.isNotEmpty) { + final allTabBars = [...state.tabBars]; + final tabBarControllerByViewId = { + ...state.tabBarControllerByViewId + }; + var newSelectedIndex = state.selectedIndex; + for (final viewId in updatePB.deleteChildViews) { + final index = allTabBars.indexWhere( + (element) => element.viewId == viewId, + ); + if (index != -1) { + final tarBar = allTabBars.removeAt(index); + // Dispose the controller when the tab is removed. + final controller = + tabBarControllerByViewId.remove(tarBar.viewId); + controller?.dispose(); + } + + if (index == state.selectedIndex) { + if (index > 0 && allTabBars.isNotEmpty) { + newSelectedIndex = index - 1; + } + } + } + emit( + state.copyWith( + tabBars: allTabBars, + selectedIndex: newSelectedIndex, + tabBarControllerByViewId: tabBarControllerByViewId, + ), + ); + } + }, + viewDidUpdate: (ViewPB updatedView) { + final index = state.tabBars.indexWhere( + (element) => element.viewId == updatedView.id, + ); + if (index != -1) { + final allTabBars = [...state.tabBars]; + final updatedTabBar = TarBar(view: updatedView); + allTabBars[index] = updatedTabBar; + emit(state.copyWith(tabBars: allTabBars)); + } + }, + ); + }, + ); + } + + @override + Future close() async { + for (final tabBar in state.tabBars) { + await state.tabBarControllerByViewId[tabBar.viewId]?.dispose(); + } + return super.close(); + } + + void _listenInlineViewChanged() { + final controller = state.tabBarControllerByViewId[state.parentView.id]; + controller?.onViewUpdated = (newView) { + add(GridTabBarEvent.viewDidUpdate(newView)); + }; + + // Only listen the child view changes when the parent view is inline. + controller?.onViewChildViewChanged = (update) { + add(GridTabBarEvent.didUpdateChildViews(update)); + }; + } + + /// Create tab bar controllers for the new views and return the updated map. + Map _extendsTabBarController( + List newViews, + ) { + final tabBarControllerByViewId = {...state.tabBarControllerByViewId}; + for (final view in newViews) { + final controller = DatabaseTarBarController(view: view); + controller.onViewUpdated = (newView) { + add(GridTabBarEvent.viewDidUpdate(newView)); + }; + + tabBarControllerByViewId[view.id] = controller; + } + return tabBarControllerByViewId; + } + + Future _createLinkedView(String name, ViewLayoutPB layoutType) async { + final viewId = state.parentView.id; + final databaseIdOrError = + await DatabaseViewBackendService(viewId: viewId).getDatabaseId(); + databaseIdOrError.fold( + (databaseId) async { + final linkedViewOrError = + await ViewBackendService.createDatabaseLinkedView( + parentViewId: viewId, + databaseId: databaseId, + layoutType: layoutType, + name: name, + ); + + linkedViewOrError.fold( + (linkedView) {}, + (err) => Log.error(err), + ); + }, + (r) => Log.error(r), + ); + } + + Future _loadChildView() async { + ViewBackendService.getChildViews(viewId: state.parentView.id) + .then((viewsOrFail) { + if (isClosed) { + return; + } + viewsOrFail.fold( + (views) => add(GridTabBarEvent.didLoadChildViews(views)), + (err) => Log.error(err), + ); + }); + } +} + +@freezed +class GridTabBarEvent with _$GridTabBarEvent { + const factory GridTabBarEvent.initial() = _Initial; + const factory GridTabBarEvent.didLoadChildViews( + List childViews, + ) = _DidLoadChildViews; + const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView; + const factory GridTabBarEvent.createView(AddButtonAction action) = + _CreateView; + const factory GridTabBarEvent.renameView(String viewId, String newName) = + _RenameView; + const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView; + const factory GridTabBarEvent.didUpdateChildViews( + ChildViewUpdatePB updatePB, + ) = _DidUpdateChildViews; + const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate; +} + +@freezed +class GridTabBarState with _$GridTabBarState { + const factory GridTabBarState({ + required ViewPB parentView, + required int selectedIndex, + required List tabBars, + required Map tabBarControllerByViewId, + }) = _GridTabBarState; + + factory GridTabBarState.initial(ViewPB view) { + final tabBar = TarBar(view: view); + return GridTabBarState( + parentView: view, + selectedIndex: 0, + tabBars: [tabBar], + tabBarControllerByViewId: { + view.id: DatabaseTarBarController( + view: view, + ) + }, + ); + } +} + +class TarBar extends Equatable { + final ViewPB view; + final DatabaseTabBarItemBuilder _builder; + + String get viewId => view.id; + DatabaseTabBarItemBuilder get builder => _builder; + ViewLayoutPB get layout => view.layout; + + TarBar({ + required this.view, + }) : _builder = view.tarBarItem(); + + @override + List get props => [view.hashCode]; +} + +typedef OnViewUpdated = void Function(ViewPB newView); +typedef OnViewChildViewChanged = void Function( + ChildViewUpdatePB childViewUpdate, +); + +class DatabaseTarBarController { + ViewPB view; + final DatabaseController controller; + final ViewListener viewListener; + OnViewUpdated? onViewUpdated; + OnViewChildViewChanged? onViewChildViewChanged; + + DatabaseTarBarController({ + required this.view, + }) : controller = DatabaseController(view: view), + viewListener = ViewListener(viewId: view.id) { + viewListener.start( + onViewChildViewsUpdated: (update) { + onViewChildViewChanged?.call(update); + }, + onViewUpdated: (newView) { + view = newView; + onViewUpdated?.call(newView); + }, + ); + } + + Future dispose() async { + await viewListener.stop(); + await controller.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart index c44caaf1af..b961750068 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -35,7 +35,7 @@ class DatabaseViewCache { final String viewId; late RowCache _rowCache; final DatabaseViewListener _databaseViewListener; - DatabaseViewCallbacks? _callbacks; + final List _callbacks = []; UnmodifiableListView get rowInfos => _rowCache.rowInfos; RowCache get rowCache => _rowCache; @@ -61,22 +61,28 @@ class DatabaseViewCache { _rowCache.applyRowsChanged(changeset); if (changeset.deletedRows.isNotEmpty) { - _callbacks?.onRowsDeleted?.call(changeset.deletedRows); + for (final callback in _callbacks) { + callback.onRowsDeleted?.call(changeset.deletedRows); + } } if (changeset.updatedRows.isNotEmpty) { - _callbacks?.onRowsUpdated?.call( - changeset.updatedRows.map((e) => e.rowId).toList(), - _rowCache.changeReason, - ); + for (final callback in _callbacks) { + callback.onRowsUpdated?.call( + changeset.updatedRows.map((e) => e.rowId).toList(), + _rowCache.changeReason, + ); + } } if (changeset.insertedRows.isNotEmpty) { - _callbacks?.onRowsCreated?.call( - changeset.insertedRows - .map((insertedRow) => insertedRow.rowMeta.id) - .toList(), - ); + for (final callback in _callbacks) { + callback.onRowsCreated?.call( + changeset.insertedRows + .map((insertedRow) => insertedRow.rowMeta.id) + .toList(), + ); + } } }, (err) => Log.error(err), @@ -103,21 +109,25 @@ class DatabaseViewCache { ); _rowCache.onRowsChanged( - (reason) => _callbacks?.onNumOfRowsChanged?.call( - rowInfos, - _rowCache.rowByRowId, - reason, - ), + (reason) { + for (final callback in _callbacks) { + callback.onNumOfRowsChanged?.call( + rowInfos, + _rowCache.rowByRowId, + reason, + ); + } + }, ); } Future dispose() async { await _databaseViewListener.stop(); await _rowCache.dispose(); - _callbacks = null; + _callbacks.clear(); } - void setListener(DatabaseViewCallbacks callbacks) { - _callbacks = callbacks; + void addListener(DatabaseViewCallbacks callbacks) { + _callbacks.add(callbacks); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index fc664bc279..ba427d4a39 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -28,9 +28,10 @@ class BoardBloc extends Bloc { FieldController get fieldController => databaseController.fieldController; String get viewId => databaseController.viewId; - BoardBloc({required ViewPB view}) - : databaseController = DatabaseController(view: view), - super(BoardState.initial(view.id)) { + BoardBloc({ + required ViewPB view, + required this.databaseController, + }) : super(BoardState.initial(view.id)) { boardController = AppFlowyBoardController( onMoveGroup: ( fromGroupId, @@ -166,7 +167,6 @@ class BoardBloc extends Bloc { @override Future close() async { - await databaseController.dispose(); for (final controller in groupControllers.values) { controller.dispose(); } @@ -233,7 +233,7 @@ class BoardBloc extends Bloc { }, ); - databaseController.setListener( + databaseController.addListener( onDatabaseChanged: onDatabaseChanged, onGroupChanged: onGroupChanged, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart index 9763d8431f..22e5858d4f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart @@ -1,19 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/util.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'presentation/board_page.dart'; class BoardPluginBuilder implements PluginBuilder { @override Plugin build(dynamic data) { if (data is ViewPB) { - return BoardPlugin(pluginType: pluginType, view: data); + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); } else { throw FlowyPluginException.invalidData; } @@ -36,55 +31,3 @@ class BoardPluginConfig implements PluginConfig { @override bool get creatable => true; } - -class BoardPlugin extends Plugin { - @override - final ViewPluginNotifier notifier; - final PluginType _pluginType; - - BoardPlugin({ - required ViewPB view, - required PluginType pluginType, - bool listenOnViewChanged = false, - }) : _pluginType = pluginType, - notifier = ViewPluginNotifier( - view: view, - listenOnViewChanged: listenOnViewChanged, - ); - - @override - PluginWidgetBuilder get widgetBuilder => - BoardPluginWidgetBuilder(notifier: notifier); - - @override - PluginId get id => notifier.view.id; - - @override - PluginType get pluginType => _pluginType; -} - -class BoardPluginWidgetBuilder extends PluginWidgetBuilder { - final ViewPluginNotifier notifier; - BoardPluginWidgetBuilder({required this.notifier, Key? key}); - - ViewPB get view => notifier.view; - - @override - Widget get leftBarItem => ViewLeftBarItem(view: view); - - @override - Widget buildWidget({PluginContext? context}) { - notifier.isDeleted.addListener(() { - notifier.isDeleted.value.fold(() => null, (deletedView) { - if (deletedView.hasIndex()) { - context?.onDeleted(view, deletedView.index); - } - }); - }); - - return BoardPage(key: ValueKey(view.id), view: view); - } - - @override - List get navigationItems => [this]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index c74d4e791c..c25d791c46 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -3,9 +3,11 @@ import 'dart:collection'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; @@ -24,11 +26,48 @@ import '../../widgets/card/card_cell_builder.dart'; import '../../widgets/row/cell_builder.dart'; import '../application/board_bloc.dart'; import '../../widgets/card/card.dart'; -import 'toolbar/board_toolbar.dart'; +import 'toolbar/board_setting_bar.dart'; + +class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ) { + return BoardPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return BoardSettingBar( + key: _makeValueKey(controller), + databaseController: controller, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return SizedBox.fromSize(); + } + + ValueKey _makeValueKey(DatabaseController controller) { + return ValueKey(controller.viewId); + } +} class BoardPage extends StatelessWidget { + final DatabaseController databaseController; BoardPage({ required this.view, + required this.databaseController, Key? key, this.onEditStateChanged, }) : super(key: ValueKey(view.id)); @@ -41,8 +80,10 @@ class BoardPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - BoardBloc(view: view)..add(const BoardEvent.initial()), + create: (context) => BoardBloc( + view: view, + databaseController: databaseController, + )..add(const BoardEvent.initial()), child: BlocBuilder( buildWhen: (p, c) => p.loadingState != c.loadingState, builder: (context, state) { @@ -110,14 +151,9 @@ class _BoardContentState extends State { child: BlocBuilder( buildWhen: (previous, current) => previous.groupIds != current.groupIds, builder: (context, state) { - final column = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [const _ToolbarBlocAdaptor(), _buildBoard(context)], - ); - return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: column, + child: _buildBoard(context), ); }, ), @@ -125,22 +161,20 @@ class _BoardContentState extends State { } Widget _buildBoard(BuildContext context) { - return Expanded( - child: AppFlowyBoard( - boardScrollController: scrollManager, - scrollController: ScrollController(), - controller: context.read().boardController, - headerBuilder: _buildHeader, - footerBuilder: _buildFooter, - cardBuilder: (_, column, columnItem) => _buildCard( - context, - column, - columnItem, - ), - groupConstraints: const BoxConstraints.tightFor(width: 300), - config: AppFlowyBoardConfig( - groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant, - ), + return AppFlowyBoard( + boardScrollController: scrollManager, + scrollController: ScrollController(), + controller: context.read().boardController, + headerBuilder: _buildHeader, + footerBuilder: _buildFooter, + cardBuilder: (_, column, columnItem) => _buildCard( + context, + column, + columnItem, + ), + groupConstraints: const BoxConstraints.tightFor(width: 300), + config: AppFlowyBoardConfig( + groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant, ), ); } @@ -335,17 +369,6 @@ class _BoardContentState extends State { } } -class _ToolbarBlocAdaptor extends StatelessWidget { - const _ToolbarBlocAdaptor({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) => const BoardToolbar(), - ); - } -} - Widget? _buildHeaderIcon(GroupData customData) { Widget? widget; switch (customData.fieldType) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart similarity index 52% rename from frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart index 6e22a77ef6..f3c357749d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/toolbar/board_setting_bar.dart @@ -1,10 +1,11 @@ -import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -class BoardToolbar extends StatelessWidget { - const BoardToolbar({ +class BoardSettingBar extends StatelessWidget { + final DatabaseController databaseController; + const BoardSettingBar({ + required this.databaseController, Key? key, }) : super(key: key); @@ -15,9 +16,7 @@ class BoardToolbar extends StatelessWidget { child: Row( children: [ const Spacer(), - SettingButton( - databaseController: context.read().databaseController, - ), + SettingButton(databaseController: databaseController), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index 63c007e55c..cf9bba21c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -27,9 +27,8 @@ class CalendarBloc extends Bloc { CellCache get cellCache => databaseController.rowCache.cellCache; RowCache get rowCache => databaseController.rowCache; - CalendarBloc({required ViewPB view}) - : databaseController = DatabaseController(view: view), - super(CalendarState.initial()) { + CalendarBloc({required ViewPB view, required this.databaseController}) + : super(CalendarState.initial()) { on( (event, emit) async { await event.when( @@ -39,6 +38,12 @@ class CalendarBloc extends Bloc { _loadAllEvents(); }, didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) { + // If the field id changed, reload all events + state.settings.fold(() => null, (oldSetting) { + if (oldSetting.fieldId != settings.fieldId) { + _loadAllEvents(); + } + }); emit(state.copyWith(settings: Some(settings))); }, didReceiveDatabaseUpdate: (DatabasePB database) { @@ -53,10 +58,6 @@ class CalendarBloc extends Bloc { ), ); }, - didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) { - _loadAllEvents(); - emit(state.copyWith(settings: Some(layoutSettings))); - }, createEvent: (DateTime date, String title) async { await _createEvent(date, title); }, @@ -105,12 +106,6 @@ class CalendarBloc extends Bloc { ); } - @override - Future close() async { - await databaseController.dispose(); - return super.close(); - } - FieldInfo? _getCalendarFieldInfo(String fieldId) { final fieldInfos = databaseController.fieldController.fieldInfos; final index = fieldInfos.indexWhere( @@ -149,7 +144,9 @@ class CalendarBloc extends Bloc { Future _createEvent(DateTime date, String title) async { return state.settings.fold( - () => null, + () { + Log.warn('Calendar settings not found'); + }, (settings) async { final dateField = _getCalendarFieldInfo(settings.fieldId); final titleField = _getTitleFieldInfo(); @@ -207,7 +204,7 @@ class CalendarBloc extends Bloc { Future _updateCalendarLayoutSetting( CalendarLayoutSettingPB layoutSetting, ) async { - return databaseController.updateCalenderLayoutSetting(layoutSetting); + return databaseController.updateLayoutSetting(layoutSetting); } Future?> _loadEvent(RowId rowId) async { @@ -333,14 +330,9 @@ class CalendarBloc extends Bloc { onLoadLayout: _didReceiveLayoutSetting, ); - final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks( - onCalendarLayoutChanged: _didReceiveNewLayoutField, - ); - - databaseController.setListener( + databaseController.addListener( onDatabaseChanged: onDatabaseChanged, onLayoutChanged: onLayoutChanged, - onCalendarLayoutChanged: onCalendarLayoutFieldChanged, ); } @@ -353,13 +345,6 @@ class CalendarBloc extends Bloc { } } - void _didReceiveNewLayoutField(DatabaseLayoutSettingPB layoutSetting) { - if (layoutSetting.hasCalendar()) { - if (isClosed) return; - add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar)); - } - } - bool isEventDayChanged(CalendarEventData event) { final index = state.allEvents.indexWhere( (element) => element.event!.eventId == event.event!.eventId, @@ -426,10 +411,6 @@ class CalendarEvent with _$CalendarEvent { const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = _ReceiveDatabaseUpdate; - - const factory CalendarEvent.didReceiveNewLayoutField( - CalendarLayoutSettingPB layoutSettings, - ) = _DidReceiveNewLayoutField; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart new file mode 100644 index 0000000000..761fdeb367 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart @@ -0,0 +1,167 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../application/database_controller.dart'; +import '../../application/row/row_cache.dart'; + +part 'unschedule_event_bloc.freezed.dart'; + +class UnscheduleEventsBloc + extends Bloc { + final DatabaseController databaseController; + Map fieldInfoByFieldId = {}; + + // Getters + String get viewId => databaseController.viewId; + FieldController get fieldController => databaseController.fieldController; + CellCache get cellCache => databaseController.rowCache.cellCache; + RowCache get rowCache => databaseController.rowCache; + + UnscheduleEventsBloc({ + required this.databaseController, + }) : super(UnscheduleEventsState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + _loadAllEvents(); + }, + didLoadAllEvents: (events) { + emit( + state.copyWith( + allEvents: events, + unscheduleEvents: + events.where((element) => !element.isScheduled).toList(), + ), + ); + }, + didDeleteEvents: (List deletedRowIds) { + final events = [...state.allEvents]; + events.retainWhere( + (element) => !deletedRowIds.contains(element.rowMeta.id), + ); + emit( + state.copyWith( + allEvents: events, + unscheduleEvents: + events.where((element) => !element.isScheduled).toList(), + ), + ); + }, + didReceiveEvent: (CalendarEventPB event) { + emit( + state.copyWith( + allEvents: [...state.allEvents, event], + ), + ); + }, + ); + }, + ); + } + + Future _loadEvent( + RowId rowId, + ) async { + final payload = RowIdPB(viewId: viewId, rowId: rowId); + return DatabaseEventGetCalendarEvent(payload).send().then( + (result) => result.fold( + (eventPB) => eventPB, + (r) { + Log.error(r); + return null; + }, + ), + ); + } + + Future _loadAllEvents() async { + final payload = CalendarEventRequestPB.create()..viewId = viewId; + DatabaseEventGetAllCalendarEvents(payload).send().then((result) { + result.fold( + (events) { + if (!isClosed) { + add(UnscheduleEventsEvent.didLoadAllEvents(events.items)); + } + }, + (r) => Log.error(r), + ); + }); + } + + void _startListening() { + final onDatabaseChanged = DatabaseCallbacks( + onRowsCreated: (rowIds) async { + if (isClosed) { + return; + } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null && !isClosed) { + add(UnscheduleEventsEvent.didReceiveEvent(event)); + } + } + }, + onRowsDeleted: (rowIds) { + if (isClosed) { + return; + } + add(UnscheduleEventsEvent.didDeleteEvents(rowIds)); + }, + onRowsUpdated: (rowIds, reason) async { + if (isClosed) { + return; + } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null) { + add(UnscheduleEventsEvent.didDeleteEvents([id])); + add(UnscheduleEventsEvent.didReceiveEvent(event)); + } + } + }, + ); + + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); + } +} + +@freezed +class UnscheduleEventsEvent with _$UnscheduleEventsEvent { + const factory UnscheduleEventsEvent.initial() = _InitialCalendar; + + // Called after loading all the current evnets + const factory UnscheduleEventsEvent.didLoadAllEvents( + List events, + ) = _ReceiveUnscheduleEventsEvents; + + const factory UnscheduleEventsEvent.didDeleteEvents(List rowIds) = + _DidDeleteEvents; + + const factory UnscheduleEventsEvent.didReceiveEvent( + CalendarEventPB event, + ) = _DidReceiveEvent; +} + +@freezed +class UnscheduleEventsState with _$UnscheduleEventsState { + const factory UnscheduleEventsState({ + required Option database, + required List allEvents, + required List unscheduleEvents, + }) = _UnscheduleEventsState; + + factory UnscheduleEventsState.initial() => UnscheduleEventsState( + database: none(), + allEvents: [], + unscheduleEvents: [], + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart index c9a4249ea7..aa5047ca2f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart @@ -1,19 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:flutter/material.dart'; - -import '../../util.dart'; -import 'presentation/calendar_page.dart'; class CalendarPluginBuilder extends PluginBuilder { @override Plugin build(dynamic data) { if (data is ViewPB) { - return CalendarPlugin(pluginType: pluginType, view: data); + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); } else { throw FlowyPluginException.invalidData; } @@ -36,55 +31,3 @@ class CalendarPluginConfig implements PluginConfig { @override bool get creatable => true; } - -class CalendarPlugin extends Plugin { - @override - final ViewPluginNotifier notifier; - final PluginType _pluginType; - - CalendarPlugin({ - required ViewPB view, - required PluginType pluginType, - bool listenOnViewChanged = false, - }) : _pluginType = pluginType, - notifier = ViewPluginNotifier( - view: view, - listenOnViewChanged: listenOnViewChanged, - ); - - @override - PluginWidgetBuilder get widgetBuilder => - CalendarPluginWidgetBuilder(notifier: notifier); - - @override - PluginId get id => notifier.view.id; - - @override - PluginType get pluginType => _pluginType; -} - -class CalendarPluginWidgetBuilder extends PluginWidgetBuilder { - final ViewPluginNotifier notifier; - CalendarPluginWidgetBuilder({required this.notifier, Key? key}); - - ViewPB get view => notifier.view; - - @override - Widget get leftBarItem => ViewLeftBarItem(view: view); - - @override - Widget buildWidget({PluginContext? context}) { - notifier.isDeleted.addListener(() { - notifier.isDeleted.value.fold(() => null, (deletedView) { - if (deletedView.hasIndex()) { - context?.onDeleted(view, deletedView.index); - } - }); - }); - - return CalendarPage(key: ValueKey(view.id), view: view); - } - - @override - List get navigationItems => [this]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart index 241e276c57..be0e2b575c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -309,7 +309,7 @@ class _EventCard extends StatelessWidget { cellBuilder: cellBuilder, openCard: (context) => showEventDetails( context: context, - event: event, + event: event.event, viewId: viewId, rowCache: rowCache, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index 5efc020bd2..562251a150 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -1,5 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -15,11 +18,51 @@ import '../../widgets/row/cell_builder.dart'; import '../../widgets/row/row_detail.dart'; import 'calendar_day.dart'; import 'layout/sizes.dart'; -import 'toolbar/calendar_toolbar.dart'; +import 'toolbar/calendar_setting_bar.dart'; + +class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ) { + return CalendarPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return CalendarSettingBar( + key: _makeValueKey(controller), + databaseController: controller, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return SizedBox.fromSize(); + } + + ValueKey _makeValueKey(DatabaseController controller) { + return ValueKey(controller.viewId); + } +} class CalendarPage extends StatefulWidget { final ViewPB view; - const CalendarPage({required this.view, super.key}); + final DatabaseController databaseController; + const CalendarPage({ + required this.view, + required this.databaseController, + super.key, + }); @override State createState() => _CalendarPageState(); @@ -33,8 +76,10 @@ class _CalendarPageState extends State { @override void initState() { _calendarState = GlobalKey(); - _calendarBloc = CalendarBloc(view: widget.view) - ..add(const CalendarEvent.initial()); + _calendarBloc = CalendarBloc( + view: widget.view, + databaseController: widget.databaseController, + )..add(const CalendarEvent.initial()); super.initState(); } @@ -79,7 +124,7 @@ class _CalendarPageState extends State { if (state.editingEvent != null) { showEventDetails( context: context, - event: state.editingEvent!.event!, + event: state.editingEvent!.event!.event, viewId: widget.view.id, rowCache: _calendarBloc.rowCache, ); @@ -115,8 +160,6 @@ class _CalendarPageState extends State { builder: (context, state) { return Column( children: [ - // const _ToolbarBlocAdaptor(), - const CalendarToolbar(), _buildCalendar( _eventController, state.settings @@ -238,12 +281,12 @@ class _CalendarPageState extends State { void showEventDetails({ required BuildContext context, - required CalendarDayEvent event, + required CalendarEventPB event, required String viewId, required RowCache rowCache, }) { final dataController = RowController( - rowMeta: event.event.rowMeta, + rowMeta: event.rowMeta, viewId: viewId, rowCache: rowCache, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart index 8264b899f7..79f718f9e2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -351,21 +351,16 @@ class FirstDayOfWeek extends StatelessWidget { final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; // starts from sunday - final items = symbols.WEEKDAYS.asMap().entries.map((entry) { - final index = entry.key; - final string = entry.value; - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.medium(string), - onTap: () { - onUpdated(index); - popoverMutex.close(); - }, - rightIcon: firstDayOfWeek == index - ? const FlowySvg(name: 'grid/checkmark') - : null, - ), + const len = 2; + final items = symbols.WEEKDAYS.take(len).indexed.map((entry) { + return StartFromButton( + title: entry.$2, + dayIndex: entry.$1, + isSelected: firstDayOfWeek == entry.$1, + onTap: (index) { + onUpdated(index); + popoverMutex.close(); + }, ); }).toList(); @@ -376,7 +371,7 @@ class FirstDayOfWeek extends StatelessWidget { itemBuilder: (context, index) => items[index], separatorBuilder: (context, index) => VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: 2, + itemCount: len, ), ); }, @@ -426,3 +421,29 @@ enum CalendarLayoutSettingAction { showWeekNumber, showTimeLine, } + +class StartFromButton extends StatelessWidget { + final int dayIndex; + final String title; + final bool isSelected; + final void Function(int) onTap; + const StartFromButton({ + required this.title, + required this.dayIndex, + required this.onTap, + required this.isSelected, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(title), + onTap: () => onTap(dayIndex), + rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart new file mode 100644 index 0000000000..ec74f739e6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting_bar.dart @@ -0,0 +1,178 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CalendarSettingBar extends StatelessWidget { + final DatabaseController databaseController; + const CalendarSettingBar({ + required this.databaseController, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + UnscheduleEventsButton(databaseController: databaseController), + SettingButton( + databaseController: databaseController, + ), + ], + ), + ); + } +} + +class UnscheduleEventsButton extends StatefulWidget { + final DatabaseController databaseController; + const UnscheduleEventsButton({ + required this.databaseController, + Key? key, + }) : super(key: key); + + @override + State createState() => _UnscheduleEventsButtonState(); +} + +class _UnscheduleEventsButtonState extends State { + late final PopoverController _popoverController; + late final UnscheduleEventsBloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = UnscheduleEventsBloc(databaseController: widget.databaseController) + ..add(const UnscheduleEventsEvent.initial()); + _popoverController = PopoverController(); + } + + @override + dispose() { + _bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithCenterAligned, + controller: _popoverController, + offset: const Offset(0, 8), + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), + child: BlocProvider.value( + value: _bloc, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.unscheduleEvents.length != + current.unscheduleEvents.length, + builder: (context, state) { + return FlowyTextButton( + "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})", + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + ); + }, + ), + ), + popupBuilder: (context) { + return UnscheduleEventsList( + viewId: _bloc.viewId, + rowCache: _bloc.rowCache, + controller: _popoverController, + unscheduleEvents: _bloc.state.unscheduleEvents, + ); + }, + ); + } +} + +class UnscheduleEventsList extends StatelessWidget { + final String viewId; + final RowCache rowCache; + final PopoverController controller; + final List unscheduleEvents; + const UnscheduleEventsList({ + required this.viewId, + required this.controller, + required this.unscheduleEvents, + required this.rowCache, + super.key, + }); + + @override + Widget build(BuildContext context) { + final cells = [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: FlowyText.medium( + LocaleKeys.calendar_settings_clickToAdd.tr(), + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + const VSpace(6), + ...unscheduleEvents.map( + (e) => UnscheduledEventCell( + event: e, + onPressed: () { + showEventDetails( + context: context, + event: e, + viewId: viewId, + rowCache: rowCache, + ); + controller.close(); + }, + ), + ) + ]; + + return ListView.separated( + itemBuilder: (context, index) => cells[index], + itemCount: cells.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + shrinkWrap: true, + ); + } +} + +class UnscheduledEventCell extends StatelessWidget { + final CalendarEventPB event; + final VoidCallback onPressed; + const UnscheduledEventCell({ + required this.event, + required this.onPressed, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium( + event.title.isEmpty + ? LocaleKeys.calendar_defaultNewCalendarTitle.tr() + : event.title, + ), + onTap: onPressed, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart deleted file mode 100644 index 42ee25acf7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; -import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:calendar_view/calendar_view.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/calendar_bloc.dart'; - -class CalendarToolbar extends StatelessWidget { - const CalendarToolbar({super.key}); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 40, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const _UnscheduleEventsButton(), - SettingButton( - databaseController: context.read().databaseController, - ), - ], - ), - ); - } -} - -class _UnscheduleEventsButton extends StatefulWidget { - const _UnscheduleEventsButton({Key? key}) : super(key: key); - - @override - State<_UnscheduleEventsButton> createState() => - _UnscheduleEventsButtonState(); -} - -class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> { - late final PopoverController _controller; - - @override - void initState() { - super.initState(); - _controller = PopoverController(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final unscheduledEvents = state.allEvents - .where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0)) - .toList(); - final viewId = context.read().viewId; - final rowCache = context.read().rowCache; - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - controller: _controller, - offset: const Offset(0, 8), - constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), - child: FlowyTextButton( - "${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})", - fillColor: Colors.transparent, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - padding: GridSize.typeOptionContentInsets, - ), - popupBuilder: (context) { - final cells = [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: FlowyText.medium( - // LocaleKeys.calendar_settings_noDateHint.tr(), - LocaleKeys.calendar_settings_clickToAdd.tr(), - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - const VSpace(6), - ...unscheduledEvents.map( - (e) => _UnscheduledEventItem( - event: e, - onPressed: () { - showEventDetails( - context: context, - event: e.event!, - viewId: viewId, - rowCache: rowCache, - ); - _controller.close(); - }, - ), - ) - ]; - return ListView.separated( - itemBuilder: (context, index) => cells[index], - itemCount: cells.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - shrinkWrap: true, - ); - }, - ); - }, - ); - } -} - -class _UnscheduledEventItem extends StatelessWidget { - final CalendarEventData event; - final VoidCallback onPressed; - const _UnscheduledEventItem({ - required this.event, - required this.onPressed, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.medium( - event.title.isEmpty - ? LocaleKeys.calendar_defaultNewCalendarTitle.tr() - : event.title, - ), - onTap: onPressed, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart index 993e42a3b0..199c4e2252 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_accessory_bloc.dart @@ -3,17 +3,17 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'grid_accessory_bloc.freezed.dart'; -class GridAccessoryMenuBloc - extends Bloc { +class DatabaseViewSettingExtensionBloc extends Bloc< + DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> { final String viewId; - GridAccessoryMenuBloc({required this.viewId}) + DatabaseViewSettingExtensionBloc({required this.viewId}) : super( - GridAccessoryMenuState.initial( + DatabaseViewSettingExtensionState.initial( viewId, ), ) { - on( + on( (event, emit) async { event.when( initial: () {}, @@ -27,22 +27,25 @@ class GridAccessoryMenuBloc } @freezed -class GridAccessoryMenuEvent with _$GridAccessoryMenuEvent { - const factory GridAccessoryMenuEvent.initial() = _Initial; - const factory GridAccessoryMenuEvent.toggleMenu() = _MenuVisibleChange; +class DatabaseViewSettingExtensionEvent + with _$DatabaseViewSettingExtensionEvent { + const factory DatabaseViewSettingExtensionEvent.initial() = _Initial; + const factory DatabaseViewSettingExtensionEvent.toggleMenu() = + _MenuVisibleChange; } @freezed -class GridAccessoryMenuState with _$GridAccessoryMenuState { - const factory GridAccessoryMenuState({ +class DatabaseViewSettingExtensionState + with _$DatabaseViewSettingExtensionState { + const factory DatabaseViewSettingExtensionState({ required String viewId, required bool isVisible, - }) = _GridAccessoryMenuState; + }) = _DatabaseViewSettingExtensionState; - factory GridAccessoryMenuState.initial( + factory DatabaseViewSettingExtensionState.initial( String viewId, ) => - GridAccessoryMenuState( + DatabaseViewSettingExtensionState( viewId: viewId, isVisible: false, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index 92f3da700a..4907611004 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -65,17 +67,27 @@ class GridBloc extends Bloc { ), ); }, + didReceveFilters: (List filters) { + emit( + state.copyWith( + reorderable: filters.isEmpty && state.sorts.isEmpty, + filters: filters, + ), + ); + }, + didReceveSorts: (List sorts) { + emit( + state.copyWith( + reorderable: sorts.isEmpty && state.filters.isEmpty, + sorts: sorts, + ), + ); + }, ); }, ); } - @override - Future close() async { - await databaseController.dispose(); - return super.close(); - } - RowCache getRowCache(RowId rowId) { return databaseController.rowCache; } @@ -93,17 +105,29 @@ class GridBloc extends Bloc { } }, onRowsUpdated: (rows, reason) { - add( - GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason), - ); + if (!isClosed) { + add( + GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason), + ); + } }, onFieldsChanged: (fields) { if (!isClosed) { add(GridEvent.didReceiveFieldUpdate(fields)); } }, + onFiltersChanged: (filters) { + if (!isClosed) { + add(GridEvent.didReceveFilters(filters)); + } + }, + onSortsChanged: (sorts) { + if (!isClosed) { + add(GridEvent.didReceveSorts(sorts)); + } + }, ); - databaseController.setListener(onDatabaseChanged: onDatabaseChanged); + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); } Future _openGrid(Emitter emit) async { @@ -138,6 +162,11 @@ class GridEvent with _$GridEvent { const factory GridEvent.didReceiveGridUpdate( DatabasePB grid, ) = _DidReceiveGridUpdate; + + const factory GridEvent.didReceveFilters(List filters) = + _DidReceiveFilters; + const factory GridEvent.didReceveSorts(List sorts) = + _DidReceiveSorts; } @freezed @@ -149,7 +178,10 @@ class GridState with _$GridState { required List rowInfos, required int rowCount, required GridLoadingState loadingState, + required bool reorderable, required RowsChangedReason reason, + required List sorts, + required List filters, }) = _GridState; factory GridState.initial(String viewId) => GridState( @@ -158,8 +190,11 @@ class GridState with _$GridState { rowCount: 0, grid: none(), viewId: viewId, + reorderable: true, loadingState: const _Loading(), reason: const InitialListState(), + filters: [], + sorts: [], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart index bb44b3aedf..453f773796 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart @@ -1,19 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/util.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:flutter/material.dart'; - -import 'presentation/grid_page.dart'; class GridPluginBuilder implements PluginBuilder { @override Plugin build(dynamic data) { if (data is ViewPB) { - return GridPlugin(pluginType: pluginType, view: data); + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); } else { throw FlowyPluginException.invalidData; } @@ -36,55 +31,3 @@ class GridPluginConfig implements PluginConfig { @override bool get creatable => true; } - -class GridPlugin extends Plugin { - @override - final ViewPluginNotifier notifier; - final PluginType _pluginType; - - GridPlugin({ - required ViewPB view, - required PluginType pluginType, - bool listenOnViewChanged = false, - }) : _pluginType = pluginType, - notifier = ViewPluginNotifier( - view: view, - listenOnViewChanged: listenOnViewChanged, - ); - - @override - PluginWidgetBuilder get widgetBuilder => - GridPluginWidgetBuilder(notifier: notifier); - - @override - PluginId get id => notifier.view.id; - - @override - PluginType get pluginType => _pluginType; -} - -class GridPluginWidgetBuilder extends PluginWidgetBuilder { - final ViewPluginNotifier notifier; - ViewPB get view => notifier.view; - - GridPluginWidgetBuilder({required this.notifier, Key? key}); - - @override - Widget get leftBarItem => ViewLeftBarItem(view: view); - - @override - Widget buildWidget({PluginContext? context}) { - notifier.isDeleted.addListener(() { - notifier.isDeleted.value.fold(() => null, (deletedView) { - if (deletedView.hasIndex()) { - context?.onDeleted(view, deletedView.index); - } - }); - }); - - return GridPage(key: ValueKey(view.id), view: view); - } - - @override - List get navigationItems => [this]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 9b4e11dea4..836543d7ac 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -1,5 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -15,25 +17,77 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import '../../application/field/field_controller.dart'; import '../../application/row/row_cache.dart'; import '../../application/row/row_data_controller.dart'; -import '../../application/setting/setting_bloc.dart'; -import '../application/filter/filter_menu_bloc.dart'; import '../application/grid_bloc.dart'; import '../../application/database_controller.dart'; -import '../application/sort/sort_menu_bloc.dart'; import 'grid_scroll.dart'; +import '../../tar_bar/tab_bar_view.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; -import 'widgets/accessory_menu.dart'; import 'widgets/row/row.dart'; import 'widgets/footer/grid_footer.dart'; import 'widgets/header/grid_header.dart'; import '../../widgets/row/row_detail.dart'; import 'widgets/shortcuts.dart'; -import 'widgets/toolbar/grid_toolbar.dart'; + +class ToggleExtensionNotifier extends ChangeNotifier { + bool _isToggled = false; + + get isToggled => _isToggled; + + void toggle() { + _isToggled = !_isToggled; + notifyListeners(); + } +} + +class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { + final _toggleExtension = ToggleExtensionNotifier(); + + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ) { + return GridPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return GridSettingBar( + key: _makeValueKey(controller), + controller: controller, + toggleExtension: _toggleExtension, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return DatabaseViewSettingExtension( + key: _makeValueKey(controller), + viewId: controller.viewId, + databaseController: controller, + toggleExtension: _toggleExtension, + ); + } + + ValueKey _makeValueKey(DatabaseController controller) { + return ValueKey(controller.viewId); + } +} class GridPage extends StatefulWidget { + final DatabaseController databaseController; const GridPage({ required this.view, + required this.databaseController, this.onDeleted, Key? key, }) : super(key: key); @@ -46,12 +100,9 @@ class GridPage extends StatefulWidget { } class _GridPageState extends State { - late DatabaseController databaseController; - @override void initState() { super.initState(); - databaseController = DatabaseController(view: widget.view); } @override @@ -61,24 +112,9 @@ class _GridPageState extends State { BlocProvider( create: (context) => GridBloc( view: widget.view, - databaseController: databaseController, + databaseController: widget.databaseController, )..add(const GridEvent.initial()), ), - BlocProvider( - create: (context) => GridFilterMenuBloc( - viewId: widget.view.id, - fieldController: databaseController.fieldController, - )..add(const GridFilterMenuEvent.initial()), - ), - BlocProvider( - create: (context) => SortMenuBloc( - viewId: widget.view.id, - fieldController: databaseController.fieldController, - )..add(const SortMenuEvent.initial()), - ), - BlocProvider( - create: (context) => DatabaseSettingBloc(viewId: widget.view.id), - ), ], child: BlocBuilder( builder: (context, state) { @@ -87,9 +123,7 @@ class _GridPageState extends State { const Center(child: CircularProgressIndicator.adaptive()), finish: (result) => result.successOrFail.fold( (_) => GridShortcuts( - child: FlowyGrid( - viewId: widget.view.id, - ), + child: GridPageContent(view: widget.view), ), (err) => FlowyErrorPage(err.toString()), ), @@ -100,18 +134,18 @@ class _GridPageState extends State { } } -class FlowyGrid extends StatefulWidget { - final String viewId; - const FlowyGrid({ - required this.viewId, +class GridPageContent extends StatefulWidget { + final ViewPB view; + const GridPageContent({ + required this.view, super.key, }); @override - State createState() => _FlowyGridState(); + State createState() => _GridPageContentState(); } -class _FlowyGridState extends State { +class _GridPageContentState extends State { final _scrollController = GridScrollController( scrollGroupController: LinkedScrollControllerGroup(), ); @@ -135,106 +169,114 @@ class _FlowyGridState extends State { buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { final contentWidth = GridLayout.headerWidth(state.fields.value); - final child = _WrapScrollView( - scrollController: _scrollController, - contentWidth: contentWidth, - child: _GridRows( - viewId: widget.viewId, - verticalScrollController: _scrollController.verticalController, - ), - ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const GridToolbar(), - GridAccessoryMenu(viewId: state.viewId), - _gridHeader(context, state.viewId), - Flexible(child: child), - const _RowCountBadge(), + _GridHeader(headerScrollController: headerScrollController), + _GridRows( + viewId: state.viewId, + contentWidth: contentWidth, + scrollController: _scrollController, + ), + const _GridFooter(), ], ); }, ); } +} - Widget _gridHeader(BuildContext context, String viewId) { - final fieldController = - context.read().databaseController.fieldController; - return GridHeaderSliverAdaptor( - viewId: viewId, - fieldController: fieldController, - anchorScrollController: headerScrollController, +class _GridHeader extends StatelessWidget { + final ScrollController headerScrollController; + const _GridHeader({required this.headerScrollController}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return GridHeaderSliverAdaptor( + viewId: state.viewId, + fieldController: + context.read().databaseController.fieldController, + anchorScrollController: headerScrollController, + ); + }, ); } } class _GridRows extends StatelessWidget { final String viewId; + final double contentWidth; + final GridScrollController scrollController; + const _GridRows({ required this.viewId, - required this.verticalScrollController, + required this.contentWidth, + required this.scrollController, }); - final ScrollController verticalScrollController; - @override Widget build(BuildContext context) { - final filterState = context.watch().state; - final sortState = context.watch().state; - - return BlocBuilder( - buildWhen: (previous, current) => current.reason.maybeWhen( - reorderRows: () => true, - reorderSingleRow: (reorderRow, rowInfo) => true, - delete: (item) => true, - insert: (item) => true, - orElse: () => false, - ), - builder: (context, state) { - final rowInfos = state.rowInfos; - final behavior = ScrollConfiguration.of(context).copyWith( - scrollbars: false, - ); - return ScrollConfiguration( - behavior: behavior, - child: ReorderableListView.builder( - /// TODO(Xazin): Resolve inconsistent scrollbar behavior - /// This is a workaround related to - /// https://github.com/flutter/flutter/issues/25652 - cacheExtent: 5000, - scrollController: verticalScrollController, - buildDefaultDragHandles: false, - proxyDecorator: (child, index, animation) => Material( - color: Colors.white.withOpacity(.1), - child: Opacity(opacity: .5, child: child), - ), - onReorder: (fromIndex, newIndex) { - final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; - if (fromIndex == toIndex) { - return; - } - context - .read() - .add(GridEvent.moveRow(fromIndex, toIndex)); - }, - itemCount: rowInfos.length + 1, // the extra item is the footer - itemBuilder: (context, index) { - if (index < rowInfos.length) { - final rowInfo = rowInfos[index]; - return _renderRow( - context, - rowInfo.rowId, - index: index, - isSortEnabled: sortState.sortInfos.isNotEmpty, - isFilterEnabled: filterState.filters.isNotEmpty, - ); - } - return const _GridFooter(key: Key('gridFooter')); - }, + return Flexible( + child: _WrapScrollView( + scrollController: scrollController, + contentWidth: contentWidth, + child: BlocBuilder( + buildWhen: (previous, current) => current.reason.maybeWhen( + reorderRows: () => true, + reorderSingleRow: (reorderRow, rowInfo) => true, + delete: (item) => true, + insert: (item) => true, + orElse: () => false, ), - ); - }, + builder: (context, state) { + final rowInfos = state.rowInfos; + final behavior = ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ); + return ScrollConfiguration( + behavior: behavior, + child: ReorderableListView.builder( + /// TODO(Xazin): Resolve inconsistent scrollbar behavior + /// This is a workaround related to + /// https://github.com/flutter/flutter/issues/25652 + cacheExtent: 5000, + scrollController: scrollController.verticalController, + buildDefaultDragHandles: false, + proxyDecorator: (child, index, animation) => Material( + color: Colors.white.withOpacity(.1), + child: Opacity(opacity: .5, child: child), + ), + onReorder: (fromIndex, newIndex) { + final toIndex = + newIndex > fromIndex ? newIndex - 1 : newIndex; + if (fromIndex == toIndex) { + return; + } + context + .read() + .add(GridEvent.moveRow(fromIndex, toIndex)); + }, + itemCount: rowInfos.length + 1, // the extra item is the footer + itemBuilder: (context, index) { + if (index < rowInfos.length) { + final rowInfo = rowInfos[index]; + return _renderRow( + context, + rowInfo.rowId, + isDraggable: state.reorderable, + index: index, + ); + } + return const GridRowBottomBar(key: Key('gridFooter')); + }, + ), + ); + }, + ), + ), ); } @@ -242,8 +284,7 @@ class _GridRows extends StatelessWidget { BuildContext context, RowId rowId, { int? index, - bool isSortEnabled = false, - bool isFilterEnabled = false, + required bool isDraggable, Animation? animation, }) { final rowCache = context.read().getRowCache(rowId); @@ -265,7 +306,7 @@ class _GridRows extends StatelessWidget { rowId: rowId, viewId: viewId, index: index, - isDraggable: !isSortEnabled && !isFilterEnabled, + isDraggable: isDraggable, dataController: dataController, cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), openDetailPage: (context, cellBuilder) { @@ -320,22 +361,6 @@ class _GridRows extends StatelessWidget { } } -class _GridFooter extends StatelessWidget { - const _GridFooter({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: GridSize.footerContentInsets, - height: GridSize.footerHeight, - margin: const EdgeInsets.only(bottom: 200), - child: const GridAddRowButton(), - ); - } -} - class _WrapScrollView extends StatelessWidget { const _WrapScrollView({ required this.contentWidth, @@ -366,8 +391,8 @@ class _WrapScrollView extends StatelessWidget { } } -class _RowCountBadge extends StatelessWidget { - const _RowCountBadge(); +class _GridFooter extends StatelessWidget { + const _GridFooter(); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/accessory_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/accessory_menu.dart deleted file mode 100644 index 22aea38118..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/accessory_menu.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../layout/sizes.dart'; -import 'filter/filter_menu.dart'; -import 'sort/sort_menu.dart'; - -class GridAccessoryMenu extends StatelessWidget { - final String viewId; - const GridAccessoryMenu({required this.viewId, Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => GridAccessoryMenuBloc(viewId: viewId), - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => p.isVisible != c.isVisible, - listener: (context, state) => context - .read() - .add(const GridAccessoryMenuEvent.toggleMenu()), - ), - BlocListener( - listenWhen: (p, c) => p.isVisible != c.isVisible, - listener: (context, state) => context - .read() - .add(const GridAccessoryMenuEvent.toggleMenu()), - ), - ], - child: BlocBuilder( - builder: (context, state) { - if (state.isVisible) { - return const _AccessoryMenu(); - } else { - return const SizedBox(); - } - }, - ), - ), - ); - } -} - -class _AccessoryMenu extends StatelessWidget { - const _AccessoryMenu({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return _wrapPadding( - Column( - children: [ - Divider( - height: 1.0, - color: AFThemeExtension.of(context).toggleOffFill, - ), - const VSpace(6), - const IntrinsicHeight( - child: Row( - children: [ - SortMenu(), - HSpace(6), - FilterMenu(), - ], - ), - ), - ], - ), - ); - }, - ); - } - - Widget _wrapPadding(Widget child) { - return Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.leadingHeaderPadding, - vertical: 6, - ), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart index 91cbcbe54b..8df5fc9add 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -12,37 +13,49 @@ import 'create_filter_list.dart'; import 'filter_menu_item.dart'; class FilterMenu extends StatelessWidget { - const FilterMenu({Key? key}) : super(key: key); + final FieldController fieldController; + const FilterMenu({ + required this.fieldController, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final List children = []; - children.addAll( - state.filters - .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) - .toList(), - ); + return BlocProvider( + create: (context) => GridFilterMenuBloc( + viewId: fieldController.viewId, + fieldController: fieldController, + )..add( + const GridFilterMenuEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + final List children = []; + children.addAll( + state.filters + .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) + .toList(), + ); - if (state.creatableFields.isNotEmpty) { - children.add(AddFilterButton(viewId: state.viewId)); - } + if (state.creatableFields.isNotEmpty) { + children.add(AddFilterButton(viewId: state.viewId)); + } - return Expanded( - child: Row( - children: [ - Expanded( - child: Wrap( - spacing: 6, - runSpacing: 4, - children: children, + return Expanded( + child: Row( + children: [ + Expanded( + child: Wrap( + spacing: 6, + runSpacing: 4, + children: children, + ), ), - ), - ], - ), - ); - }, + ], + ), + ); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart index b985f59430..62f250b43b 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/image.dart'; @@ -27,3 +28,19 @@ class GridAddRowButton extends StatelessWidget { ); } } + +class GridRowBottomBar extends StatelessWidget { + const GridRowBottomBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: GridSize.footerContentInsets, + height: GridSize.footerHeight, + margin: const EdgeInsets.only(bottom: 200), + child: const GridAddRowButton(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart index c255e36525..3ebebcac00 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_menu.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -13,30 +14,40 @@ import 'sort_editor.dart'; import 'sort_info.dart'; class SortMenu extends StatelessWidget { - const SortMenu({Key? key}) : super(key: key); + final FieldController fieldController; + const SortMenu({ + required this.fieldController, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.sortInfos.isNotEmpty) { - return AppFlowyPopover( - controller: PopoverController(), - constraints: BoxConstraints.loose(const Size(340, 200)), - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (BuildContext popoverContext) { - return SortEditor( - viewId: state.viewId, - fieldController: context.read().fieldController, - sortInfos: state.sortInfos, - ); - }, - child: SortChoiceChip(sortInfos: state.sortInfos), - ); - } else { - return const SizedBox(); - } - }, + return BlocProvider( + create: (context) => SortMenuBloc( + viewId: fieldController.viewId, + fieldController: fieldController, + )..add(const SortMenuEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + if (state.sortInfos.isNotEmpty) { + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(340, 200)), + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext popoverContext) { + return SortEditor( + viewId: state.viewId, + fieldController: context.read().fieldController, + sortInfos: state.sortInfos, + ); + }, + child: SortChoiceChip(sortInfos: state.sortInfos), + ); + } else { + return const SizedBox(); + } + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart index b110b3cc87..04786deeea 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_layout.dart @@ -1,7 +1,6 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart'; +import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -67,34 +66,6 @@ class _DatabaseLayoutListState extends State { } } -extension DatabaseLayoutExtension on DatabaseLayoutPB { - String layoutName() { - switch (this) { - case DatabaseLayoutPB.Board: - return LocaleKeys.board_menuName.tr(); - case DatabaseLayoutPB.Calendar: - return LocaleKeys.calendar_menuName.tr(); - case DatabaseLayoutPB.Grid: - return LocaleKeys.grid_menuName.tr(); - default: - return ""; - } - } - - String iconName() { - switch (this) { - case DatabaseLayoutPB.Board: - return 'editor/board'; - case DatabaseLayoutPB.Calendar: - return "editor/grid"; - case DatabaseLayoutPB.Grid: - return "editor/grid"; - default: - return ""; - } - } -} - class DatabaseViewLayoutCell extends StatelessWidget { final bool isSelected; final DatabaseLayoutPB databaseLayout; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart new file mode 100644 index 0000000000..cb093eae34 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart @@ -0,0 +1,68 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'filter_button.dart'; +import 'sort_button.dart'; + +class GridSettingBar extends StatelessWidget { + final DatabaseController controller; + final ToggleExtensionNotifier toggleExtension; + const GridSettingBar({ + required this.controller, + required this.toggleExtension, + super.key, + }); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => GridFilterMenuBloc( + viewId: controller.viewId, + fieldController: controller.fieldController, + )..add(const GridFilterMenuEvent.initial()), + ), + BlocProvider( + create: (context) => SortMenuBloc( + viewId: controller.viewId, + fieldController: controller.fieldController, + )..add(const SortMenuEvent.initial()), + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + ), + BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + ), + ], + child: SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: GridSize.leadingHeaderPadding), + const Spacer(), + const FilterButton(), + const SortButton(), + SettingButton( + databaseController: controller, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart deleted file mode 100644 index 4f572e65ed..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_toolbar.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../layout/sizes.dart'; -import 'filter_button.dart'; -import '../../../../widgets/setting/setting_button.dart'; -import 'sort_button.dart'; - -class GridToolbarContext { - final String viewId; - final FieldController fieldController; - GridToolbarContext({ - required this.viewId, - required this.fieldController, - }); -} - -class GridToolbar extends StatelessWidget { - const GridToolbar({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 40, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(width: GridSize.leadingHeaderPadding), - const Spacer(), - const FilterButton(), - const SortButton(), - SettingButton( - databaseController: context.read().databaseController, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart new file mode 100644 index 0000000000..0defa34026 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/setting_menu.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; + +import '../application/field/field_controller.dart'; +import '../grid/presentation/layout/sizes.dart'; +import '../grid/presentation/widgets/filter/filter_menu.dart'; +import '../grid/presentation/widgets/sort/sort_menu.dart'; + +class DatabaseViewSettingExtension extends StatelessWidget { + final String viewId; + final DatabaseController databaseController; + final ToggleExtensionNotifier toggleExtension; + const DatabaseViewSettingExtension({ + required this.viewId, + required this.databaseController, + required this.toggleExtension, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: toggleExtension, + child: Consumer( + builder: (context, value, child) { + if (value.isToggled) { + return BlocProvider( + create: (context) => + DatabaseViewSettingExtensionBloc(viewId: viewId), + child: _DatabaseViewSettingContent( + fieldController: databaseController.fieldController, + ), + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } +} + +class _DatabaseViewSettingContent extends StatelessWidget { + final FieldController fieldController; + const _DatabaseViewSettingContent({ + required this.fieldController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final children = [ + Divider( + height: 1.0, + color: AFThemeExtension.of(context).toggleOffFill, + ), + const VSpace(6), + IntrinsicHeight( + child: Row( + children: [ + SortMenu( + fieldController: fieldController, + ), + const HSpace(6), + FilterMenu( + fieldController: fieldController, + ), + ], + ), + ) + ]; + + return _wrapPadding( + Column(children: children), + ); + }, + ); + } + + Widget _wrapPadding(Widget child) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.leadingHeaderPadding, + vertical: 6, + ), + child: child, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart new file mode 100644 index 0000000000..4a5e6a0cb2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart @@ -0,0 +1,415 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart'; +import 'package:appflowy/plugins/util.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../application/database_controller.dart'; +import '../grid/presentation/layout/sizes.dart'; +import 'tar_bar_add_button.dart'; + +abstract class DatabaseTabBarItemBuilder { + const DatabaseTabBarItemBuilder(); + + /// Returns the content of the tab bar item. The content is shown when the tab + /// bar item is selected. It can be any kind of database view. + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + ); + + /// Returns the setting bar of the tab bar item. The setting bar is shown on the + /// top right conner when the tab bar item is selected. + Widget settingBar( + BuildContext context, + DatabaseController controller, + ); + + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ); +} + +class DatabaseTabBarView extends StatefulWidget { + final ViewPB view; + const DatabaseTabBarView({ + required this.view, + super.key, + }); + + @override + State createState() => _DatabaseTabBarViewState(); +} + +class _DatabaseTabBarViewState extends State { + PageController? _pageController; + + @override + void initState() { + super.initState(); + _pageController = PageController( + initialPage: 0, + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => GridTabBarBloc(view: widget.view) + ..add( + const GridTabBarEvent.initial(), + ), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.selectedIndex != c.selectedIndex, + listener: (context, state) { + _pageController?.animateToPage( + state.selectedIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.ease, + ); + }, + ), + ], + child: Column( + children: [ + Row( + children: [ + BlocBuilder( + builder: (context, state) { + return const Flexible( + child: Padding( + padding: EdgeInsets.only(left: 50), + child: DatabaseTabBar(), + ), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + return SizedBox( + width: 300, + child: Padding( + padding: const EdgeInsets.only(right: 50), + child: pageSettingBarFromState(state), + ), + ); + }, + ), + ], + ), + BlocBuilder( + builder: (context, state) { + return pageSettingBarExtensionFromState(state); + }, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return PageView( + pageSnapping: false, + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: pageContentFromState(state), + ); + }, + ), + ), + ], + ), + ), + ); + } + + List pageContentFromState(GridTabBarState state) { + return state.tabBars.map((tabBar) { + final controller = + state.tabBarControllerByViewId[tabBar.viewId]!.controller; + return tabBar.builder.content( + context, + tabBar.view, + controller, + ); + }).toList(); + } + + Widget pageSettingBarFromState(GridTabBarState state) { + if (state.tabBars.length < state.selectedIndex) { + return const SizedBox.shrink(); + } + final tarBar = state.tabBars[state.selectedIndex]; + final controller = + state.tabBarControllerByViewId[tarBar.viewId]!.controller; + return tarBar.builder.settingBar( + context, + controller, + ); + } + + Widget pageSettingBarExtensionFromState(GridTabBarState state) { + if (state.tabBars.length < state.selectedIndex) { + return const SizedBox.shrink(); + } + final tarBar = state.tabBars[state.selectedIndex]; + final controller = + state.tabBarControllerByViewId[tarBar.viewId]!.controller; + return tarBar.builder.settingBarExtension( + context, + controller, + ); + } +} + +class DatabaseTabBarViewPlugin extends Plugin { + @override + final ViewPluginNotifier notifier; + final PluginType _pluginType; + + DatabaseTabBarViewPlugin({ + required ViewPB view, + required PluginType pluginType, + }) : _pluginType = pluginType, + notifier = ViewPluginNotifier(view: view); + + @override + PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder( + notifier: notifier, + ); + + @override + PluginId get id => notifier.view.id; + + @override + PluginType get pluginType => _pluginType; +} + +class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { + final ViewPluginNotifier notifier; + + DatabasePluginWidgetBuilder({ + required this.notifier, + Key? key, + }); + + @override + Widget get leftBarItem => ViewLeftBarItem(view: notifier.view); + + @override + Widget buildWidget({PluginContext? context}) { + notifier.isDeleted.addListener(() { + notifier.isDeleted.value.fold(() => null, (deletedView) { + if (deletedView.hasIndex()) { + context?.onDeleted(notifier.view, deletedView.index); + } + }); + }); + return DatabaseTabBarView( + key: ValueKey(notifier.view.id), + view: notifier.view, + ); + } + + @override + List get navigationItems => [this]; +} + +class DatabaseTabBar extends StatefulWidget { + const DatabaseTabBar({super.key}); + + @override + State createState() => _DatabaseTabBarState(); +} + +class _DatabaseTabBarState extends State { + final _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final children = state.tabBars.indexed.map((indexed) { + final isSelected = state.selectedIndex == indexed.$1; + final tabBar = indexed.$2; + return DatabaseTabBarItem( + key: ValueKey(tabBar.viewId), + view: tabBar.view, + isSelected: isSelected, + onTap: (selectedView) { + context.read().add( + GridTabBarEvent.selectView(selectedView.id), + ); + }, + ); + }).toList(); + + return Row( + children: [ + Flexible( + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: IntrinsicWidth( + child: Row(children: children), + ), + ), + ), + AddDatabaseViewButton( + onTap: (action) async { + context.read().add( + GridTabBarEvent.createView(action), + ); + }, + ), + ], + ); + }, + ); + } +} + +class DatabaseTabBarItem extends StatelessWidget { + final bool isSelected; + final ViewPB view; + final Function(ViewPB) onTap; + const DatabaseTabBarItem({ + required this.view, + required this.isSelected, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 80, maxWidth: 160), + child: IntrinsicWidth( + child: Column( + children: [ + TabBarItemButton( + view: view, + onTap: () => onTap(view), + ), + if (isSelected) + Divider( + height: 1, + thickness: 2, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ); + } +} + +class TabBarItemButton extends StatelessWidget { + final ViewPB view; + final VoidCallback onTap; + const TabBarItemButton({ + required this.view, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + actions: TabBarViewAction.values, + buildChild: (controller) { + return FlowyButton( + radius: Corners.s5Border, + hoverColor: AFThemeExtension.of(context).greyHover, + onTap: onTap, + onSecondaryTap: () { + controller.show(); + }, + text: FlowyText.medium( + view.name, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + margin: GridSize.cellContentInsets, + leftIcon: svgWidget( + view.iconName, + color: Theme.of(context).iconTheme.color, + ), + ); + }, + onSelected: (action, controller) { + switch (action) { + case TabBarViewAction.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.menuAppHeader_renameDialog.tr(), + value: view.name, + confirm: (newValue) { + context.read().add( + GridTabBarEvent.renameView(view.id, newValue), + ); + }, + ).show(context); + break; + case TabBarViewAction.delete: + NavigatorAlertDialog( + title: LocaleKeys.grid_deleteView.tr(), + confirm: () { + context.read().add( + GridTabBarEvent.deleteView(view.id), + ); + }, + ).show(context); + + break; + } + controller.close(); + }, + ); + } +} + +enum TabBarViewAction implements ActionCell { + rename, + delete; + + @override + String get name { + switch (this) { + case TabBarViewAction.rename: + return LocaleKeys.disclosureAction_rename.tr(); + case TabBarViewAction.delete: + return LocaleKeys.disclosureAction_delete.tr(); + } + } + + Widget icon(Color iconColor) { + switch (this) { + case TabBarViewAction.rename: + return const FlowySvg(name: 'editor/edit'); + case TabBarViewAction.delete: + return const FlowySvg(name: 'editor/delete'); + } + } + + @override + Widget? leftIcon(Color iconColor) => icon(iconColor); + + @override + Widget? rightIcon(Color iconColor) => null; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart new file mode 100644 index 0000000000..d009f7be33 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tar_bar_add_button.dart @@ -0,0 +1,156 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/extension.dart'; +import 'package:flutter/material.dart'; + +class AddDatabaseViewButton extends StatefulWidget { + final Function(AddButtonAction) onTap; + const AddDatabaseViewButton({ + required this.onTap, + super.key, + }); + + @override + State createState() => _AddDatabaseViewButtonState(); +} + +class _AddDatabaseViewButtonState extends State { + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + constraints: BoxConstraints.loose(const Size(200, 400)), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 8), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + child: FlowyIconButton( + iconPadding: const EdgeInsets.all(4), + hoverColor: AFThemeExtension.of(context).greyHover, + onPressed: () => popoverController.show(), + icon: svgWidget( + 'home/add', + color: Theme.of(context).colorScheme.tertiary, + ), + ), + popupBuilder: (BuildContext context) { + return TarBarAddButtonAction( + onTap: (action) { + popoverController.close(); + widget.onTap(action); + }, + ); + }, + ); + } +} + +class TarBarAddButtonAction extends StatelessWidget { + final Function(AddButtonAction) onTap; + const TarBarAddButtonAction({ + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + final cells = AddButtonAction.values.map((layout) { + return TarBarAddButtonActionCell( + action: layout, + onTap: onTap, + ); + }).toList(); + + return ListView.separated( + controller: ScrollController(), + shrinkWrap: true, + itemCount: cells.length, + itemBuilder: (BuildContext context, int index) => cells[index], + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + padding: const EdgeInsets.symmetric(vertical: 6.0), + ); + } +} + +class TarBarAddButtonActionCell extends StatelessWidget { + final AddButtonAction action; + final void Function(AddButtonAction) onTap; + const TarBarAddButtonActionCell({ + required this.action, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: FlowyText.medium( + action.title, + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: svgWidget( + action.iconName, + color: Theme.of(context).iconTheme.color, + ), + onTap: () => onTap(action), + ).padding(horizontal: 6.0), + ); + } +} + +enum AddButtonAction { + grid, + calendar, + board; + + String get title { + switch (this) { + case AddButtonAction.board: + return LocaleKeys.board_menuName.tr(); + case AddButtonAction.calendar: + return LocaleKeys.calendar_menuName.tr(); + case AddButtonAction.grid: + return LocaleKeys.grid_menuName.tr(); + default: + return ""; + } + } + + ViewLayoutPB get layoutType { + switch (this) { + case AddButtonAction.board: + return ViewLayoutPB.Board; + case AddButtonAction.calendar: + return ViewLayoutPB.Calendar; + case AddButtonAction.grid: + return ViewLayoutPB.Grid; + default: + return ViewLayoutPB.Grid; + } + } + + String get iconName { + switch (this) { + case AddButtonAction.board: + return 'editor/board'; + case AddButtonAction.calendar: + return "editor/grid"; + case AddButtonAction.grid: + return "editor/grid"; + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart new file mode 100644 index 0000000000..43b0faa6bf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_layout_ext.dart @@ -0,0 +1,31 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension DatabaseLayoutExtension on DatabaseLayoutPB { + String layoutName() { + switch (this) { + case DatabaseLayoutPB.Board: + return LocaleKeys.board_menuName.tr(); + case DatabaseLayoutPB.Calendar: + return LocaleKeys.calendar_menuName.tr(); + case DatabaseLayoutPB.Grid: + return LocaleKeys.grid_menuName.tr(); + default: + return ""; + } + } + + String iconName() { + switch (this) { + case DatabaseLayoutPB.Board: + return 'editor/board'; + case DatabaseLayoutPB.Calendar: + return "editor/grid"; + case DatabaseLayoutPB.Grid: + return "editor/grid"; + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart index 5ae82dc390..ea51d1f675 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/database_setting.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -9,6 +8,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import '../../grid/presentation/layout/sizes.dart'; +import 'setting_button.dart'; class DatabaseSettingList extends StatelessWidget { final DatabaseController databaseContoller; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart index 62d2371d45..db6ce38f28 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/setting/setting_button.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart'; import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -97,7 +97,7 @@ class _DatabaseSettingListPopoverState case DatabaseSettingAction.showLayout: return DatabaseLayoutList( viewId: widget.databaseController.viewId, - currentLayout: widget.databaseController.databaseLayout!, + currentLayout: widget.databaseController.databaseLayout, ); case DatabaseSettingAction.showGroup: return DatabaseGroupList( @@ -132,7 +132,7 @@ class ICalendarSettingImpl extends ICalendarSetting { @override void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) { - _databaseController.updateCalenderLayoutSetting(layoutSettings); + _databaseController.updateLayoutSetting(layoutSettings); } @override @@ -140,3 +140,63 @@ class ICalendarSettingImpl extends ICalendarSetting { return _databaseController.databaseLayoutSetting?.calendar; } } + +enum DatabaseSettingAction { + showProperties, + showLayout, + showGroup, + showCalendarLayout, +} + +extension DatabaseSettingActionExtension on DatabaseSettingAction { + String iconName() { + switch (this) { + case DatabaseSettingAction.showProperties: + return 'grid/setting/properties'; + case DatabaseSettingAction.showLayout: + return 'grid/setting/database_layout'; + case DatabaseSettingAction.showGroup: + return 'grid/setting/group'; + case DatabaseSettingAction.showCalendarLayout: + return 'grid/setting/calendar_layout'; + } + } + + String title() { + switch (this) { + case DatabaseSettingAction.showProperties: + return LocaleKeys.grid_settings_Properties.tr(); + case DatabaseSettingAction.showLayout: + return LocaleKeys.grid_settings_databaseLayout.tr(); + case DatabaseSettingAction.showGroup: + return LocaleKeys.grid_settings_group.tr(); + case DatabaseSettingAction.showCalendarLayout: + return LocaleKeys.calendar_settings_name.tr(); + } + } +} + +/// Returns the list of actions that should be shown for the given database layout. +List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { + switch (layout) { + case DatabaseLayoutPB.Board: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + DatabaseSettingAction.showGroup, + ]; + case DatabaseLayoutPB.Calendar: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + DatabaseSettingAction.showCalendarLayout, + ]; + case DatabaseLayoutPB.Grid: + return [ + DatabaseSettingAction.showProperties, + DatabaseSettingAction.showLayout, + ]; + default: + return []; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 15101017aa..f1ae0ca431 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -50,10 +50,7 @@ class DocumentPlugin extends Plugin { required ViewPB view, bool listenOnViewChanged = false, Key? key, - }) : notifier = ViewPluginNotifier( - view: view, - listenOnViewChanged: listenOnViewChanged, - ) { + }) : notifier = ViewPluginNotifier(view: view) { _pluginType = pluginType; _documentAppearanceCubit.fetch(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index ce29cc7bea..82a1b9016f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -91,10 +91,12 @@ class _BuiltInPageWidgetState extends State { onExit: (_) => widget.editorState.service.scrollService?.enable(), child: SizedBox( height: 400, - child: Stack( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildMenu(context, viewPB), - _buildPage(context, viewPB), + Expanded(child: _buildPage(context, viewPB)), ], ), ), @@ -114,68 +116,58 @@ class _BuiltInPageWidgetState extends State { } Widget _buildMenu(BuildContext context, ViewPB viewPB) { - return Positioned( - top: 5, - left: 5, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // information - FlowyIconButton( - tooltipText: LocaleKeys.tooltip_referencePage.tr( - namedArgs: {'name': viewPB.layout.name}, - ), + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // information + FlowyIconButton( + tooltipText: LocaleKeys.tooltip_referencePage.tr( + namedArgs: {'name': viewPB.layout.name}, + ), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + icon: svgWidget( + 'common/information', + color: Theme.of(context).iconTheme.color, + ), + ), + // setting + const Space(7, 0), + PopoverActionList<_ActionWrapper>( + direction: PopoverDirection.bottomWithCenterAligned, + actions: _ActionType.values + .map((action) => _ActionWrapper(action)) + .toList(), + buildChild: (controller) => FlowyIconButton( + tooltipText: LocaleKeys.tooltip_openMenu.tr(), width: 24, height: 24, iconPadding: const EdgeInsets.all(3), icon: svgWidget( - 'common/information', + 'common/settings', color: Theme.of(context).iconTheme.color, ), + onPressed: () => controller.show(), ), - // Name - const Space(7, 0), - FlowyText.medium( - viewPB.name, - fontSize: 16.0, - ), - // setting - const Space(7, 0), - PopoverActionList<_ActionWrapper>( - direction: PopoverDirection.bottomWithCenterAligned, - actions: _ActionType.values - .map((action) => _ActionWrapper(action)) - .toList(), - buildChild: (controller) => FlowyIconButton( - tooltipText: LocaleKeys.tooltip_openMenu.tr(), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - icon: svgWidget( - 'common/settings', - color: Theme.of(context).iconTheme.color, - ), - onPressed: () => controller.show(), - ), - onSelected: (action, controller) async { - switch (action.inner) { - case _ActionType.viewDatabase: - getIt().latestOpenView = viewPB; + onSelected: (action, controller) async { + switch (action.inner) { + case _ActionType.viewDatabase: + getIt().latestOpenView = viewPB; - getIt().setPlugin(viewPB.plugin()); - break; - case _ActionType.delete: - final transaction = widget.editorState.transaction; - transaction.deleteNode(widget.node); - widget.editorState.apply(transaction); - break; - } - controller.close(); - }, - ) - ], - ), + getIt().setPlugin(viewPB.plugin()); + break; + case _ActionType.delete: + final transaction = widget.editorState.transaction; + transaction.deleteNode(widget.node); + widget.editorState.apply(transaction); + break; + } + controller.close(); + }, + ) + ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index 25aa43ab77..93ac919f97 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -53,7 +53,7 @@ extension InsertDatabase on EditorState { } final prefix = _referencedDatabasePrefix(childView.layout); - final ref = await ViewBackendService.createDatabaseReferenceView( + final ref = await ViewBackendService.createDatabaseLinkedView( parentViewId: childView.id, name: "$prefix ${childView.name}", layoutType: childView.layout, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 14a3afd0be..715ebb6f20 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -212,7 +213,7 @@ class _LinkToPageMenuState extends State { FlowyButton( isSelected: index == _selectedIndex, leftIcon: svgWidget( - _iconName(value), + value.iconName, color: Theme.of(context).iconTheme.color, ), text: FlowyText.regular(value.name), @@ -238,19 +239,6 @@ class _LinkToPageMenuState extends State { future: items, ); } - - String _iconName(ViewPB viewPB) { - switch (viewPB.layout) { - case ViewLayoutPB.Grid: - return 'editor/grid'; - case ViewLayoutPB.Board: - return 'editor/board'; - case ViewLayoutPB.Calendar: - return 'editor/calendar'; - default: - throw Exception('Unknown layout type'); - } - } } extension on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/lib/plugins/util.dart b/frontend/appflowy_flutter/lib/plugins/util.dart index f64fe56a75..75fea12158 100644 --- a/frontend/appflowy_flutter/lib/plugins/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/util.dart @@ -1,14 +1,10 @@ import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; -import '../workspace/presentation/home/home_stack.dart'; - class ViewPluginNotifier extends PluginNotifier> { final ViewListener? _viewListener; ViewPB view; @@ -18,30 +14,18 @@ class ViewPluginNotifier extends PluginNotifier> { ViewPluginNotifier({ required this.view, - required bool listenOnViewChanged, }) : _viewListener = ViewListener(viewId: view.id) { - if (listenOnViewChanged) { - _viewListener?.start( - onViewUpdated: (updatedView) { - // If the layout is changed, we need to create a new plugin for it. - if (view.layout != updatedView.layout) { - getIt().setPlugin( - updatedView.plugin( - listenOnViewChanged: listenOnViewChanged, - ), - ); - } else { - view = updatedView; - } - }, - onViewMoveToTrash: (result) { - result.fold( - (deletedView) => isDeleted.value = some(deletedView), - (err) => Log.error(err), - ); - }, - ); - } + _viewListener?.start( + onViewUpdated: (updatedView) { + view = updatedView; + }, + onViewMoveToTrash: (result) { + result.fold( + (deletedView) => isDeleted.value = some(deletedView), + (err) => Log.error(err), + ); + }, + ); } @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart index c246a9d81f..84a61334c1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/app/app_bloc.dart @@ -1,9 +1,7 @@ import 'dart:collection'; -import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/app/app_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; import 'package:expandable/expandable.dart'; import 'package:appflowy_backend/log.dart'; @@ -18,11 +16,11 @@ part 'app_bloc.freezed.dart'; class AppBloc extends Bloc { final ViewBackendService appService; - final AppListener appListener; + final ViewListener viewListener; AppBloc({required ViewPB view}) : appService = ViewBackendService(), - appListener = AppListener(viewId: view.id), + viewListener = ViewListener(viewId: view.id), super(AppState.initial(view)) { on((event, emit) async { await event.map( @@ -47,10 +45,10 @@ class AppBloc extends Bloc { }, appDidUpdate: (e) async { final latestCreatedView = state.latestCreatedView; - final views = e.app.childViews; + final views = e.view.childViews; AppState newState = state.copyWith( views: views, - view: e.app, + view: e.view, ); if (latestCreatedView != null) { final index = views @@ -67,8 +65,8 @@ class AppBloc extends Bloc { } void _startListening() { - appListener.start( - onAppUpdated: (app) { + viewListener.start( + onViewUpdated: (app) { if (!isClosed) { add(AppEvent.appDidUpdate(app)); } @@ -110,7 +108,7 @@ class AppBloc extends Bloc { parentViewId: state.view.id, name: value.name, desc: value.desc ?? "", - layoutType: value.pluginBuilder.layoutType!, + layoutType: value.layoutType, initialDataBytes: value.initialDataBytes, ext: value.ext ?? {}, openAfterCreate: true, @@ -131,13 +129,13 @@ class AppBloc extends Bloc { @override Future close() async { - await appListener.stop(); + await viewListener.stop(); return super.close(); } Future _loadViews(Emitter emit) async { final viewsOrFailed = - await ViewBackendService.getViews(viewId: state.view.id); + await ViewBackendService.getChildViews(viewId: state.view.id); viewsOrFailed.fold( (views) => emit(state.copyWith(views: views)), (error) { @@ -153,7 +151,7 @@ class AppEvent with _$AppEvent { const factory AppEvent.initial() = Initial; const factory AppEvent.createView( String name, - PluginBuilder pluginBuilder, { + ViewLayoutPB layoutType, { String? desc, /// ~~The initial data should be the JSON of the document~~ @@ -172,7 +170,7 @@ class AppEvent with _$AppEvent { const factory AppEvent.delete() = DeleteApp; const factory AppEvent.deleteView(String viewId) = DeleteView; const factory AppEvent.rename(String newName) = Rename; - const factory AppEvent.appDidUpdate(ViewPB app) = AppDidUpdate; + const factory AppEvent.appDidUpdate(ViewPB view) = AppDidUpdate; } @freezed @@ -191,7 +189,7 @@ class AppState with _$AppState { ); } -class AppViewDataContext extends ChangeNotifier { +class ViewDataContext extends ChangeNotifier { final String viewId; final ValueNotifier> _viewsNotifier = ValueNotifier([]); final ValueNotifier _selectedViewNotifier = ValueNotifier(null); @@ -199,7 +197,7 @@ class AppViewDataContext extends ChangeNotifier { ExpandableController expandController = ExpandableController(initialExpanded: false); - AppViewDataContext({required this.viewId}) { + ViewDataContext({required this.viewId}) { _setLatestView(getIt().latestOpenView); _menuSharedStateListener = getIt().addLatestViewListener((view) { @@ -207,7 +205,7 @@ class AppViewDataContext extends ChangeNotifier { }); } - VoidCallback addSelectedViewChangeListener(void Function(ViewPB?) callback) { + VoidCallback onViewSelected(void Function(ViewPB?) callback) { listener() { callback(_selectedViewNotifier.value); } @@ -216,7 +214,7 @@ class AppViewDataContext extends ChangeNotifier { return listener; } - void removeSelectedViewListener(VoidCallback listener) { + void removeOnViewSelectedListener(VoidCallback listener) { _selectedViewNotifier.removeListener(listener); } @@ -235,7 +233,6 @@ class AppViewDataContext extends ChangeNotifier { set views(List views) { if (_viewsNotifier.value != views) { _viewsNotifier.value = views; - _expandIfNeed(); notifyListeners(); } } @@ -243,7 +240,7 @@ class AppViewDataContext extends ChangeNotifier { UnmodifiableListView get views => UnmodifiableListView(_viewsNotifier.value); - VoidCallback addViewsChangeListener( + VoidCallback onViewsChanged( void Function(UnmodifiableListView) callback, ) { listener() { @@ -254,7 +251,7 @@ class AppViewDataContext extends ChangeNotifier { return listener; } - void removeViewsListener(VoidCallback listener) { + void removeOnViewChangedListener(VoidCallback listener) { _viewsNotifier.removeListener(listener); } @@ -263,7 +260,10 @@ class AppViewDataContext extends ChangeNotifier { return; } - if (!_viewsNotifier.value.contains(_selectedViewNotifier.value)) { + if (!_viewsNotifier.value + .map((e) => e.id) + .toList() + .contains(_selectedViewNotifier.value?.id)) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/app_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/app/app_listener.dart deleted file mode 100644 index 4de2500d07..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/app/app_listener.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; -import 'package:appflowy/core/notification/folder_notification.dart'; -import 'package:dartz/dartz.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; - -typedef AppDidUpdateCallback = void Function(ViewPB app); -typedef ViewsDidChangeCallback = void Function( - Either, FlowyError> viewsOrFailed, -); - -class AppListener { - StreamSubscription? _subscription; - AppDidUpdateCallback? _updated; - FolderNotificationParser? _parser; - String viewId; - - AppListener({ - required this.viewId, - }); - - void start({AppDidUpdateCallback? onAppUpdated}) { - _updated = onAppUpdated; - _parser = FolderNotificationParser(id: viewId, callback: _handleCallback); - _subscription = - RustStreamReceiver.listen((observable) => _parser?.parse(observable)); - } - - void _handleCallback( - FolderNotification ty, - Either result, - ) { - switch (ty) { - case FolderNotification.DidUpdateView: - case FolderNotification.DidUpdateChildViews: - if (_updated != null) { - result.fold( - (payload) { - final app = ViewPB.fromBuffer(payload); - _updated!(app); - }, - (error) => Log.error(error), - ); - } - break; - default: - break; - } - } - - Future stop() async { - _parser = null; - await _subscription?.cancel(); - _updated = null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart index b8dbdc67e0..9365c03e5f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/app/prelude.dart @@ -1,2 +1 @@ export 'app_bloc.dart'; -export 'app_listener.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart index 33ddf97a08..a5b7130dc8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_view_section_bloc.dart @@ -12,65 +12,58 @@ part 'menu_view_section_bloc.freezed.dart'; class ViewSectionBloc extends Bloc { void Function()? _viewsListener; void Function()? _selectedViewlistener; - final AppViewDataContext _appViewData; + final ViewDataContext _appViewData; ViewSectionBloc({ - required AppViewDataContext appViewData, + required ViewDataContext appViewData, }) : _appViewData = appViewData, super(ViewSectionState.initial(appViewData)) { on((event, emit) async { - await event.map( - initial: (e) async { + await event.when( + initial: () async { _startListening(); }, - setSelectedView: (_SetSelectedView value) { - _setSelectView(value, emit); + setSelectedView: (view) { + emit(state.copyWith(selectedView: view)); }, - didReceiveViewUpdated: (_DidReceiveViewUpdated value) { - emit(state.copyWith(views: value.views)); + didReceiveViewUpdated: (views) { + emit(state.copyWith(views: views)); }, - moveView: (_MoveView value) async { - _moveView(value, emit); + moveView: (fromIndex, toIndex) async { + _moveView(fromIndex, toIndex, emit); }, ); }); } void _startListening() { - _viewsListener = _appViewData.addViewsChangeListener((views) { + _viewsListener = _appViewData.onViewsChanged((views) { if (!isClosed) { add(ViewSectionEvent.didReceiveViewUpdated(views)); } }); - _selectedViewlistener = _appViewData.addSelectedViewChangeListener((view) { + _selectedViewlistener = _appViewData.onViewSelected((view) { if (!isClosed) { add(ViewSectionEvent.setSelectedView(view)); } }); } - void _setSelectView(_SetSelectedView value, Emitter emit) { - if (state.views.contains(value.view)) { - emit(state.copyWith(selectedView: value.view)); - } else { - emit(state.copyWith(selectedView: null)); - } - } - Future _moveView( - _MoveView value, + int fromIndex, + int toIndex, Emitter emit, ) async { - if (value.fromIndex < state.views.length) { - final viewId = state.views[value.fromIndex].id; + if (fromIndex < state.views.length) { + final viewId = state.views[fromIndex].id; final views = List.from(state.views); - views.insert(value.toIndex, views.removeAt(value.fromIndex)); + views.insert(toIndex, views.removeAt(fromIndex)); emit(state.copyWith(views: views)); final result = await ViewBackendService.moveView( viewId: viewId, - fromIndex: value.fromIndex, - toIndex: value.toIndex, + fromIndex: fromIndex, + toIndex: toIndex, ); result.fold((l) => null, (err) => Log.error(err)); } @@ -79,11 +72,11 @@ class ViewSectionBloc extends Bloc { @override Future close() async { if (_selectedViewlistener != null) { - _appViewData.removeSelectedViewListener(_selectedViewlistener!); + _appViewData.removeOnViewSelectedListener(_selectedViewlistener!); } if (_viewsListener != null) { - _appViewData.removeViewsListener(_viewsListener!); + _appViewData.removeOnViewChangedListener(_viewsListener!); } return super.close(); @@ -108,7 +101,7 @@ class ViewSectionState with _$ViewSectionState { ViewPB? selectedView, }) = _ViewSectionState; - factory ViewSectionState.initial(AppViewDataContext appViewData) => + factory ViewSectionState.initial(ViewDataContext appViewData) => ViewSectionState( views: appViewData.views, selectedView: appViewData.selectedView, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 0fb2bd20d4..065b55d8ff 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -1,6 +1,7 @@ -import 'package:appflowy/plugins/database_view/board/board.dart'; -import 'package:appflowy/plugins/database_view/calendar/calendar.dart'; -import 'package:appflowy/plugins/database_view/grid/grid.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:flowy_infra/image.dart'; @@ -62,22 +63,11 @@ extension ViewExtension on ViewPB { Plugin plugin({bool listenOnViewChanged = false}) { switch (layout) { case ViewLayoutPB.Board: - return BoardPlugin( - view: this, - pluginType: pluginType, - listenOnViewChanged: listenOnViewChanged, - ); case ViewLayoutPB.Calendar: - return CalendarPlugin( - view: this, - pluginType: pluginType, - listenOnViewChanged: listenOnViewChanged, - ); case ViewLayoutPB.Grid: - return GridPlugin( + return DatabaseTabBarViewPlugin( view: this, pluginType: pluginType, - listenOnViewChanged: listenOnViewChanged, ); case ViewLayoutPB.Document: return DocumentPlugin( @@ -88,4 +78,31 @@ extension ViewExtension on ViewPB { } throw UnimplementedError; } + + DatabaseTabBarItemBuilder tarBarItem() { + switch (layout) { + case ViewLayoutPB.Board: + return BoardPageTabBarBuilderImpl(); + case ViewLayoutPB.Calendar: + return CalendarPageTabBarBuilderImpl(); + case ViewLayoutPB.Grid: + return GridPageTabBarBuilderImpl(); + case ViewLayoutPB.Document: + throw UnimplementedError; + } + throw UnimplementedError; + } + + String get iconName { + switch (layout) { + case ViewLayoutPB.Grid: + return 'editor/grid'; + case ViewLayoutPB.Board: + return 'editor/board'; + case ViewLayoutPB.Calendar: + return 'editor/calendar'; + default: + throw Exception('Unknown layout type'); + } + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart index 78159c96b4..fdb9bc593e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart @@ -21,6 +21,7 @@ typedef MoveToTrashNotifiedValue = Either; class ViewListener { StreamSubscription? _subscription; void Function(UpdateViewNotifiedValue)? _updatedViewNotifier; + void Function(ChildViewUpdatePB)? _updateViewChildViewsNotifier; void Function(DeleteViewNotifyValue)? _deletedNotifier; void Function(RestoreViewNotifiedValue)? _restoredNotifier; void Function(MoveToTrashNotifiedValue)? _moveToTrashNotifier; @@ -35,6 +36,7 @@ class ViewListener { void start({ void Function(UpdateViewNotifiedValue)? onViewUpdated, + void Function(ChildViewUpdatePB)? onViewChildViewsUpdated, void Function(DeleteViewNotifyValue)? onViewDeleted, void Function(RestoreViewNotifiedValue)? onViewRestored, void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash, @@ -48,6 +50,7 @@ class ViewListener { _deletedNotifier = onViewDeleted; _restoredNotifier = onViewRestored; _moveToTrashNotifier = onViewMoveToTrash; + _updateViewChildViewsNotifier = onViewChildViewsUpdated; _parser = FolderNotificationParser( id: viewId, @@ -74,6 +77,15 @@ class ViewListener { (error) => Log.error(error), ); break; + case FolderNotification.DidUpdateChildViews: + result.fold( + (payload) { + final pb = ChildViewUpdatePB.fromBuffer(payload); + _updateViewChildViewsNotifier?.call(pb); + }, + (error) => Log.error(error), + ); + break; case FolderNotification.DidDeleteView: result.fold( (payload) => _deletedNotifier?.call(left(ViewPB.fromBuffer(payload))), diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 42d046e849..6e8f71eb14 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -73,7 +73,7 @@ class ViewBackendService { return FolderEventCreateOrphanView(payload).send(); } - static Future> createDatabaseReferenceView({ + static Future> createDatabaseLinkedView({ required String parentViewId, required String databaseId, required ViewLayoutPB layoutType, @@ -91,14 +91,14 @@ class ViewBackendService { } /// Returns a list of views that are the children of the given [viewId]. - static Future, FlowyError>> getViews({ + static Future, FlowyError>> getChildViews({ required String viewId, }) { final payload = ViewIdPB.create()..value = viewId; return FolderEventReadView(payload).send().then((result) { return result.fold( - (app) => left(app.childViews), + (view) => left(view.childViews), (error) => right(error), ); }); @@ -163,7 +163,7 @@ class ViewBackendService { if (workspaces != null) { final views = workspaces.workspace.views; for (final view in views) { - final childViews = await getViews(viewId: view.id).then( + final childViews = await getChildViews(viewId: view.id).then( (value) => value .getLeftOrNull>() ?.where((e) => e.layout == layoutType) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart index eaef826273..82ffd93d40 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/header.dart @@ -113,7 +113,7 @@ class MenuAppHeader extends StatelessWidget { context.read().add( AppEvent.createView( name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - pluginBuilder, + pluginBuilder.layoutType!, initialDataBytes: initialDataBytes, openAfterCreated: openAfterCreated, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart index 1c051815c1..953e66437b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/menu_app.dart @@ -17,11 +17,11 @@ class MenuApp extends StatefulWidget { } class _MenuAppState extends State { - late AppViewDataContext viewDataContext; + late ViewDataContext viewDataContext; @override void initState() { - viewDataContext = AppViewDataContext(viewId: widget.view.id); + viewDataContext = ViewDataContext(viewId: widget.view.id); super.initState(); } @@ -56,7 +56,7 @@ class _MenuAppState extends State { builder: (context, state) { return ChangeNotifierProvider.value( value: viewDataContext, - child: Consumer( + child: Consumer( builder: (context, viewDataContext, _) { return expandableWrapper(context, viewDataContext); }, @@ -70,7 +70,7 @@ class _MenuAppState extends State { ExpandableNotifier expandableWrapper( BuildContext context, - AppViewDataContext viewDataContext, + ViewDataContext viewDataContext, ) { return ExpandableNotifier( controller: viewDataContext.expandController, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart index 611753704d..acd8adf682 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/item.dart @@ -24,12 +24,12 @@ class ViewSectionItem extends StatelessWidget { final ViewPB view; final void Function(ViewPB) onSelected; - ViewSectionItem({ + const ViewSectionItem({ Key? key, required this.view, required this.isSelected, required this.onSelected, - }) : super(key: ValueKey('$view.hashCode/$isSelected')); + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart index 20788159de..780fd0d686 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart @@ -11,7 +11,7 @@ import 'package:reorderables/reorderables.dart'; import 'item.dart'; class ViewSection extends StatelessWidget { - final AppViewDataContext appViewData; + final ViewDataContext appViewData; const ViewSection({Key? key, required this.appViewData}) : super(key: key); @override @@ -47,10 +47,11 @@ class ViewSection extends StatelessWidget { ViewSectionState state, ) { final children = state.views.map((view) { + final isSelected = _isViewSelected(state, view.id); return ViewSectionItem( - key: ValueKey(view.id), view: view, - isSelected: _isViewSelected(state, view.id), + key: ValueKey('$view.hashCode/$isSelected'), + isSelected: isSelected, onSelected: (view) => getIt().latestOpenView = view, ); }).toList(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 258d7eddc6..c1444b531e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; class FlowyButton extends StatelessWidget { final Widget text; final VoidCallback? onTap; + final VoidCallback? onSecondaryTap; final void Function(bool)? onHover; final EdgeInsets? margin; final Widget? leftIcon; @@ -25,6 +26,7 @@ class FlowyButton extends StatelessWidget { Key? key, required this.text, this.onTap, + this.onSecondaryTap, this.onHover, this.margin, this.leftIcon, @@ -45,6 +47,7 @@ class FlowyButton extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, + onSecondaryTap: onSecondaryTap, child: FlowyHover( style: HoverStyle( borderRadius: radius ?? Corners.s6Border, diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart index 16cd6b8673..556ad69420 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,8 +13,11 @@ void main() { test('create kanban baord card', () async { final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final databaseController = DatabaseController(view: context.gridView); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: databaseController, + )..add(const BoardEvent.initial()); await boardResponseFuture(); final groupId = boardBloc.state.groupIds.first; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart index f9d6af8911..6b851f0207 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; @@ -14,8 +15,10 @@ void main() { test('create build-in kanban board test', () async { final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); await boardResponseFuture(); assert(boardBloc.groupControllers.values.length == 4); @@ -24,8 +27,10 @@ void main() { test('edit kanban board field name test', () async { final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); await boardResponseFuture(); final fieldInfo = context.singleSelectFieldContext(); @@ -58,8 +63,10 @@ void main() { test('create a new field in kanban board test', () async { final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); await boardResponseFuture(); await context.createField(FieldType.Checkbox); diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart index 74af637a60..dca6f39d14 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; @@ -15,8 +16,10 @@ void main() { // Group by checkbox field test('group by checkbox field test', () async { final context = await boardTest.createTestBoard(); - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); await boardResponseFuture(); // assert the initial values diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart index 3741ea259f..f8443ddf77 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart'; @@ -39,8 +40,10 @@ void main() { await boardResponseFuture(); //assert only have the 'No status' group - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); await boardResponseFuture(); assert( boardBloc.groupControllers.values.length == 1, @@ -92,8 +95,10 @@ void main() { await boardResponseFuture(); // assert there are only three group - final boardBloc = BoardBloc(view: context.gridView) - ..add(const BoardEvent.initial()); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); await boardResponseFuture(); assert( boardBloc.groupControllers.values.length == 3, diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart index 5bf03f12ec..44a6307e9b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:bloc_test/bloc_test.dart'; @@ -39,8 +40,12 @@ void main() { ); blocTest( 'assert the number of groups is 1', - build: () => - BoardBloc(view: context.gridView)..add(const BoardEvent.initial()), + build: () => BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add( + const BoardEvent.initial(), + ), wait: boardResponseDuration(), verify: (bloc) { assert( diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart index 5f74f7540e..826a191bf2 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart @@ -1,16 +1,15 @@ import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:appflowy/plugins/database_view/grid/grid.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pbenum.dart'; import '../util.dart'; Future createTestFilterGrid(AppFlowyGridTest gridTest) async { final app = await gridTest.unitTest.createTestApp(); - final builder = GridPluginBuilder(); final context = await ViewBackendService.createView( parentViewId: app.id, name: "Filter Grid", - layoutType: builder.layoutType!, + layoutType: ViewLayoutPB.Grid, openAfterCreate: true, ).then((result) { return result.fold( diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index 9471afcbf1..c3ca8408eb 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -9,7 +9,6 @@ import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; -import 'package:appflowy/plugins/database_view/grid/grid.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; @@ -172,11 +171,10 @@ class AppFlowyGridTest { Future createTestGrid() async { final app = await unitTest.createTestApp(); - final builder = GridPluginBuilder(); final context = await ViewBackendService.createView( parentViewId: app.id, name: "Test Grid", - layoutType: builder.layoutType!, + layoutType: ViewLayoutPB.Grid, openAfterCreate: true, ).then((result) { return result.fold( diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart index b99f532e25..8913709d4c 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/app_bloc_test.dart @@ -1,9 +1,8 @@ -import 'package:appflowy/plugins/database_view/grid/grid.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; -import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../util.dart'; @@ -41,11 +40,11 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("1", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document)); await blocResponseFuture(); - bloc.add(AppEvent.createView("2", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document)); await blocResponseFuture(); - bloc.add(AppEvent.createView("3", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document)); await blocResponseFuture(); assert(bloc.state.views[0].name == '1'); @@ -58,15 +57,15 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("1", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document)); await blocResponseFuture(); - bloc.add(AppEvent.createView("2", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document)); await blocResponseFuture(); - bloc.add(AppEvent.createView("3", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("3", ViewLayoutPB.Document)); await blocResponseFuture(); assert(bloc.state.views.length == 3); - final appViewData = AppViewDataContext(viewId: app.id); + final appViewData = ViewDataContext(viewId: app.id); appViewData.views = bloc.state.views; final viewSectionBloc = ViewSectionBloc( @@ -91,14 +90,14 @@ void main() { "assert initial latest create view is null after initialize", ); - bloc.add(AppEvent.createView("1", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("1", ViewLayoutPB.Document)); await blocResponseFuture(); assert( bloc.state.latestCreatedView!.id == bloc.state.views.last.id, "create a view and assert the latest create view is this view", ); - bloc.add(AppEvent.createView("2", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("2", ViewLayoutPB.Document)); await blocResponseFuture(); assert( bloc.state.latestCreatedView!.id == bloc.state.views.last.id, @@ -111,12 +110,12 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("document 1", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document)); await blocResponseFuture(); final document1 = bloc.state.latestCreatedView; assert(document1!.name == "document 1"); - bloc.add(AppEvent.createView("document 2", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("document 2", ViewLayoutPB.Document)); await blocResponseFuture(); final document2 = bloc.state.latestCreatedView; assert(document2!.name == "document 2"); @@ -138,12 +137,12 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("document 1", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("document 1", ViewLayoutPB.Document)); await blocResponseFuture(); final document = bloc.state.latestCreatedView; assert(document!.name == "document 1"); - bloc.add(AppEvent.createView("grid 2", GridPluginBuilder())); + bloc.add(const AppEvent.createView("grid 2", ViewLayoutPB.Grid)); await blocResponseFuture(); final grid = bloc.state.latestCreatedView; assert(grid!.name == "grid 2"); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart index 98a640b6e5..3e4d092afe 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/create_page_test.dart @@ -1,7 +1,3 @@ -import 'package:appflowy/plugins/database_view/calendar/calendar.dart'; -import 'package:appflowy/plugins/database_view/board/board.dart'; -import 'package:appflowy/plugins/database_view/grid/grid.dart'; -import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -18,7 +14,7 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("Test document", DocumentPluginBuilder())); + bloc.add(const AppEvent.createView("Test document", ViewLayoutPB.Document)); await blocResponseFuture(); assert(bloc.state.views.length == 1); @@ -31,7 +27,7 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("Test grid", GridPluginBuilder())); + bloc.add(const AppEvent.createView("Test grid", ViewLayoutPB.Grid)); await blocResponseFuture(); assert(bloc.state.views.length == 1); @@ -44,7 +40,7 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("Test board", BoardPluginBuilder())); + bloc.add(const AppEvent.createView("Test board", ViewLayoutPB.Board)); await blocResponseFuture(); assert(bloc.state.views.length == 1); @@ -57,7 +53,7 @@ void main() { final bloc = AppBloc(view: app)..add(const AppEvent.initial()); await blocResponseFuture(); - bloc.add(AppEvent.createView("Test calendar", CalendarPluginBuilder())); + bloc.add(const AppEvent.createView("Test calendar", ViewLayoutPB.Calendar)); await blocResponseFuture(); assert(bloc.state.views.length == 1); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart index 6360e6f9c3..7d9f158b69 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart'; -import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy/workspace/application/home/home_bloc.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -41,7 +40,8 @@ void main() { final appBloc = AppBloc(view: app)..add(const AppEvent.initial()); assert(appBloc.state.latestCreatedView == null); - appBloc.add(AppEvent.createView("New document", DocumentPluginBuilder())); + appBloc + .add(const AppEvent.createView("New document", ViewLayoutPB.Document)); await blocResponseFuture(); assert(appBloc.state.latestCreatedView != null); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart index f4d109d21e..599877fac2 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/plugins/trash/application/trash_bloc.dart'; import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -20,25 +19,25 @@ class TrashTestContext { await blocResponseFuture(); appBloc.add( - AppEvent.createView( + const AppEvent.createView( "Document 1", - DocumentPluginBuilder(), + ViewLayoutPB.Document, ), ); await blocResponseFuture(); appBloc.add( - AppEvent.createView( + const AppEvent.createView( "Document 2", - DocumentPluginBuilder(), + ViewLayoutPB.Document, ), ); await blocResponseFuture(); appBloc.add( - AppEvent.createView( + const AppEvent.createView( "Document 3", - DocumentPluginBuilder(), + ViewLayoutPB.Document, ), ); await blocResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index 0a913688ea..d0da0bfb49 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -1,6 +1,6 @@ -import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../util.dart'; @@ -16,10 +16,7 @@ void main() { final appBloc = AppBloc(view: app)..add(const AppEvent.initial()); appBloc.add( - AppEvent.createView( - "Test document", - DocumentPluginBuilder(), - ), + const AppEvent.createView("Test document", ViewLayoutPB.Document), ); await blocResponseFuture(); @@ -38,10 +35,7 @@ void main() { await blocResponseFuture(); appBloc.add( - AppEvent.createView( - "Test document", - DocumentPluginBuilder(), - ), + const AppEvent.createView("Test document", ViewLayoutPB.Document), ); await blocResponseFuture(); @@ -61,10 +55,7 @@ void main() { await blocResponseFuture(); appBloc.add( - AppEvent.createView( - "Test document", - DocumentPluginBuilder(), - ), + const AppEvent.createView("Test document", ViewLayoutPB.Document), ); await blocResponseFuture(); expect(appBloc.state.views.length, 1); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index a059d9ed21..d129b2fee6 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -34,12 +34,12 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } #collab = { path = "../../AppFlowy-Collab/collab" } #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 2ff8ad18a4..e83b2199e2 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -85,7 +85,7 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "anyhow", "collab", @@ -887,7 +887,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "anyhow", "bytes", @@ -905,7 +905,7 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "bytes", "collab-sync", @@ -923,7 +923,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "anyhow", "async-trait", @@ -949,7 +949,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "proc-macro2", "quote", @@ -961,7 +961,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "anyhow", "collab", @@ -979,7 +979,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "anyhow", "chrono", @@ -999,7 +999,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "bincode", "chrono", @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "anyhow", "async-trait", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=06e942#06e942cb6433c94b5ecfe1d431b64bba625fc09c" dependencies = [ "bytes", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 20d766bee6..df1ed70ac4 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -33,11 +33,11 @@ opt-level = 3 incremental = false [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e9f7fc" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "06e942" } #collab = { path = "../AppFlowy-Collab/collab" } #collab-folder = { path = "../AppFlowy-Collab/collab-folder" } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs index e44bc96177..cb4b3710f4 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs @@ -276,7 +276,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { FutureResult::new(async move { database_manager - .create_linked_view(name, layout, params.database_id, database_view_id) + .create_linked_view(name, layout.into(), params.database_id, database_view_id) .await?; Ok(()) }) diff --git a/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs index 6277e344f9..e757ce4407 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs @@ -110,6 +110,9 @@ pub struct CalendarEventPB { #[pb(index = 4)] pub timestamp: i64, + + #[pb(index = 5)] + pub is_scheduled: bool, } #[derive(Debug, Clone, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index af0e9ff64e..792a19b803 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -23,6 +23,9 @@ pub struct DatabasePB { #[pb(index = 4)] pub layout_type: DatabaseLayoutPB, + + #[pb(index = 5)] + pub is_linked: bool, } #[derive(ProtoBuf, Default)] diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 1548512d4d..96f9d7f907 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -7,7 +7,7 @@ use appflowy_integrate::{CollabPersistenceConfig, RocksCollabDB}; use collab::core::collab::MutexCollab; use collab_database::database::DatabaseData; use collab_database::user::{DatabaseCollabBuilder, UserDatabase as InnerUserDatabase}; -use collab_database::views::{CreateDatabaseParams, CreateViewParams}; +use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; use parking_lot::Mutex; use tokio::sync::RwLock; @@ -16,6 +16,7 @@ use flowy_task::TaskDispatcher; use crate::entities::{DatabaseDescriptionPB, DatabaseLayoutPB, RepeatedDatabaseDescriptionPB}; use crate::services::database::{DatabaseEditor, MutexDatabase}; +use crate::services::database_view::DatabaseLayoutDepsResolver; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; pub trait DatabaseUser2: Send + Sync { @@ -179,18 +180,28 @@ impl DatabaseManager2 { Ok(()) } + /// A linked view is a view that is linked to existing database. #[tracing::instrument(level = "trace", skip(self), err)] pub async fn create_linked_view( &self, name: String, - layout: DatabaseLayoutPB, + layout: DatabaseLayout, database_id: String, database_view_id: String, ) -> FlowyResult<()> { self.with_user_database( Err(FlowyError::internal().context("Create database view failed")), |user_database| { - let params = CreateViewParams::new(database_id, database_view_id, name, layout.into()); + let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout); + if let Some(database) = user_database.get_database(&database_id) { + if let Some((field, layout_setting)) = DatabaseLayoutDepsResolver::new(database, layout) + .resolve_deps_when_create_database_linked_view() + { + params = params + .with_deps_fields(vec![field]) + .with_layout_setting(layout_setting); + } + }; user_database.create_database_linked_view(params)?; Ok(()) }, diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 72b9e2fab2..6a27a4e4eb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -206,7 +206,7 @@ impl DatabaseEditor { .map(|field| field.id) .collect() }); - database.get_fields(view_id, Some(field_ids)) + database.get_fields_in_view(view_id, Some(field_ids)) } pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { @@ -442,7 +442,7 @@ impl DatabaseEditor { self .database .lock() - .create_default_field(view_id, name, field_type.into(), |field| { + .create_field_with_mut(view_id, name, field_type.into(), |field| { field .type_options .insert(field_type.to_string(), type_option_data.clone()); @@ -932,11 +932,12 @@ impl DatabaseEditor { 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?; - view.v_update_grouping_field(field_id).await?; + view.v_grouping_by_field(field_id).await?; Ok(()) } pub async fn set_layout_setting(&self, view_id: &str, layout_setting: LayoutSettingParams) { + tracing::trace!("set_layout_setting: {:?}", layout_setting); if let Ok(view) = self.database_views.get_view_editor(view_id).await { let _ = view.v_set_layout_settings(layout_setting).await; }; @@ -1042,7 +1043,7 @@ impl DatabaseEditor { .await .ok_or_else(FlowyError::record_not_found)?; let rows = database_view.v_get_rows().await; - let (database_id, fields) = { + let (database_id, fields, is_linked) = { let database = self.database.lock(); let database_id = database.get_database_id(); let fields = database @@ -1051,7 +1052,8 @@ impl DatabaseEditor { .into_iter() .map(FieldIdPB::from) .collect(); - (database_id, fields) + let is_linked = database.is_inline_view(view_id); + (database_id, fields, is_linked) }; let rows = rows @@ -1063,6 +1065,7 @@ impl DatabaseEditor { fields, rows, layout_type: view.layout.into(), + is_linked, }) } @@ -1082,7 +1085,7 @@ impl DatabaseEditor { self .database .lock() - .get_fields(view_id, None) + .get_fields_in_view(view_id, None) .into_iter() .filter(|f| FieldType::from(f.field_type).is_auto_update()) .collect::>() @@ -1139,13 +1142,17 @@ struct DatabaseViewDataImpl { } impl DatabaseViewData for DatabaseViewDataImpl { + fn get_database(&self) -> Arc { + self.database.lock().clone() + } + fn get_view(&self, view_id: &str) -> Fut> { let view = self.database.lock().get_view(view_id); to_fut(async move { view }) } fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { - let fields = self.database.lock().get_fields(view_id, field_ids); + let fields = self.database.lock().get_fields_in_view(view_id, field_ids); to_fut(async move { fields.into_iter().map(Arc::new).collect() }) } @@ -1166,7 +1173,7 @@ impl DatabaseViewData for DatabaseViewDataImpl { field_type: FieldType, type_option_data: TypeOptionData, ) -> Fut { - let (_, field) = self.database.lock().create_default_field( + let (_, field) = self.database.lock().create_field_with_mut( view_id, name.to_string(), field_type.clone().into(), diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs new file mode 100644 index 0000000000..8f75bf2ba0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; + +use collab_database::database::{gen_field_id, Database}; +use collab_database::fields::Field; +use collab_database::views::{DatabaseLayout, LayoutSetting}; + +use crate::entities::FieldType; +use crate::services::field::DateTypeOption; +use crate::services::setting::CalendarLayoutSetting; + +/// When creating a database, we need to resolve the dependencies of the views. Different database +/// view has different dependencies. For example, a calendar view depends on a date field. +pub struct DatabaseLayoutDepsResolver { + pub database: Arc, + /// The new database layout. + pub database_layout: DatabaseLayout, +} + +impl DatabaseLayoutDepsResolver { + pub fn new(database: Arc, database_layout: DatabaseLayout) -> Self { + Self { + database, + database_layout, + } + } + + pub fn resolve_deps_when_create_database_linked_view(&self) -> Option<(Field, LayoutSetting)> { + match self.database_layout { + DatabaseLayout::Grid => None, + DatabaseLayout::Board => None, + DatabaseLayout::Calendar => { + let field = self.create_date_field(); + let layout_setting: LayoutSetting = CalendarLayoutSetting::new(field.id.clone()).into(); + Some((field, layout_setting)) + }, + } + } + + /// If the new layout type is a calendar and there is not date field in the database, it will add + /// a new date field to the database and create the corresponding layout setting. + pub fn resolve_deps_when_update_layout_type(&self, view_id: &str) { + let fields = self.database.get_fields(None); + // Insert the layout setting if it's not exist + match &self.database_layout { + DatabaseLayout::Grid => {}, + DatabaseLayout::Board => {}, + DatabaseLayout::Calendar => { + let date_field_id = match fields + .into_iter() + .find(|field| FieldType::from(field.field_type) == FieldType::DateTime) + { + None => { + tracing::trace!("Create a new date field after layout type change"); + let field = self.create_date_field(); + let field_id = field.id.clone(); + self.database.create_field(field); + field_id + }, + Some(date_field) => date_field.id, + }; + self.create_calendar_layout_setting_if_need(view_id, &date_field_id); + }, + } + } + + fn create_calendar_layout_setting_if_need(&self, view_id: &str, field_id: &str) { + if self + .database + .get_layout_setting::(view_id, &self.database_layout) + .is_none() + { + let layout_setting = CalendarLayoutSetting::new(field_id.to_string()); + self + .database + .insert_layout_setting(view_id, &self.database_layout, layout_setting); + } + } + + fn create_date_field(&self) -> Field { + let field_type = FieldType::DateTime; + let default_date_type_option = DateTypeOption::default(); + let field_id = gen_field_id(); + Field::new( + field_id, + "Date".to_string(), + field_type.clone().into(), + false, + ) + .with_type_option_data(field_type, default_date_type_option.into()) + } +} + +// pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams { +// let mut layout_setting = LayoutSettingParams::default(); +// match layout_ty { +// DatabaseLayout::Grid => {}, +// DatabaseLayout::Board => {}, +// DatabaseLayout::Calendar => { +// if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { +// let calendar_setting = CalendarLayoutSetting::from(value); +// // Check the field exist or not +// if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await { +// let field_type = FieldType::from(field.field_type); +// +// // Check the type of field is Datetime or not +// if field_type == FieldType::DateTime { +// layout_setting.calendar = Some(calendar_setting); +// } +// } +// } +// }, +// } +// +// layout_setting +// } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs index 9ee645a7cb..da4ad5bc52 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs @@ -1,3 +1,9 @@ +pub use layout_deps::*; +pub use notifier::*; +pub use view_editor::*; +pub use views::*; + +mod layout_deps; mod notifier; mod view_editor; mod view_filter; @@ -5,7 +11,3 @@ mod view_group; mod view_sort; mod views; // mod trait_impl; - -pub use notifier::*; -pub use view_editor::*; -pub use views::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 429147b499..e5a63d340a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::{gen_database_filter_id, gen_database_sort_id}; +use collab_database::database::{gen_database_filter_id, gen_database_sort_id, Database}; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cells, Row, RowCell, RowId, RowMeta}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; @@ -30,10 +30,10 @@ use crate::services::database_view::view_group::{ use crate::services::database_view::view_sort::make_sort_controller; use crate::services::database_view::{ notify_did_update_filter, notify_did_update_group_rows, notify_did_update_num_of_groups, - notify_did_update_setting, notify_did_update_sort, DatabaseViewChangedNotifier, - DatabaseViewChangedReceiverRunner, + notify_did_update_setting, notify_did_update_sort, DatabaseLayoutDepsResolver, + DatabaseViewChangedNotifier, DatabaseViewChangedReceiverRunner, }; -use crate::services::field::{DateTypeOption, TypeOptionCellDataHandler}; +use crate::services::field::TypeOptionCellDataHandler; use crate::services::filter::{ Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType, }; @@ -44,6 +44,8 @@ use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; pub trait DatabaseViewData: Send + Sync + 'static { + fn get_database(&self) -> Arc; + fn get_view(&self, view_id: &str) -> Fut>; /// If the field_ids is None, then it will return all the field revisions fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; @@ -438,7 +440,7 @@ impl DatabaseViewEditor { pub async fn v_initialize_new_group(&self, field_id: &str) -> FlowyResult<()> { let is_grouping_field = self.is_grouping_field(field_id).await; if !is_grouping_field { - self.v_update_grouping_field(field_id).await?; + self.v_grouping_by_field(field_id).await?; if let Some(view) = self.delegate.get_view(&self.view_id).await { let setting = database_view_setting_pb_from_view(view); @@ -607,7 +609,11 @@ impl DatabaseViewEditor { // Check the type of field is Datetime or not if field_type == FieldType::DateTime { layout_setting.calendar = Some(calendar_setting); + } else { + tracing::warn!("The field of calendar setting is not datetime type") } + } else { + tracing::warn!("The field of calendar setting is not exist"); } } }, @@ -713,7 +719,7 @@ impl DatabaseViewEditor { /// Called when a grouping field is updated. #[tracing::instrument(level = "debug", skip_all, err)] - pub async fn v_update_grouping_field(&self, field_id: &str) -> FlowyResult<()> { + pub async fn v_grouping_by_field(&self, field_id: &str) -> FlowyResult<()> { if let Some(field) = self.delegate.get_field(field_id).await { let new_group_controller = new_group_controller_with_field(self.view_id.clone(), self.delegate.clone(), field).await?; @@ -770,12 +776,23 @@ impl DatabaseViewEditor { date_field_id: date_field.id.clone(), title, timestamp, + is_scheduled: timestamp != 0, }) } pub async fn v_get_all_calendar_events(&self) -> Option> { let layout_ty = DatabaseLayout::Calendar; - let calendar_setting = self.v_get_layout_settings(&layout_ty).await.calendar?; + let calendar_setting = match self.v_get_layout_settings(&layout_ty).await.calendar { + None => { + // When create a new calendar view, the calendar setting should be created + tracing::error!( + "Calendar layout setting not found in database view:{}", + self.view_id + ); + return None; + }, + Some(calendar_setting) => calendar_setting, + }; // Text let primary_field = self.delegate.get_primary_field().await?; @@ -822,6 +839,7 @@ impl DatabaseViewEditor { date_field_id: calendar_setting.field_id.clone(), title, timestamp, + is_scheduled: timestamp != 0, }; events.push(event); } @@ -829,57 +847,25 @@ impl DatabaseViewEditor { } #[tracing::instrument(level = "trace", skip_all)] - pub async fn v_update_layout_type(&self, layout_type: DatabaseLayout) -> FlowyResult<()> { + pub async fn v_update_layout_type(&self, new_layout_type: DatabaseLayout) -> FlowyResult<()> { self .delegate - .update_layout_type(&self.view_id, &layout_type); + .update_layout_type(&self.view_id, &new_layout_type); - // Update the layout type in the database might add a new field to the database. If the new - // layout type is a calendar and there is not date field in the database, it will add a new - // date field to the database and create the corresponding layout setting. - // - let fields = self.delegate.get_fields(&self.view_id, None).await; - let date_field_id = match fields - .into_iter() - .find(|field| FieldType::from(field.field_type) == FieldType::DateTime) + // using the {} brackets to denote the lifetime of the resolver. Because the DatabaseLayoutDepsResolver + // is not sync and send, so we can't pass it to the async block. { - None => { - tracing::trace!("Create a new date field after layout type change"); - let default_date_type_option = DateTypeOption::default(); - let field = self - .delegate - .create_field( - &self.view_id, - "Date", - FieldType::DateTime, - default_date_type_option.into(), - ) - .await; - field.id - }, - Some(date_field) => date_field.id.clone(), - }; - - let layout_setting = self.v_get_layout_settings(&layout_type).await; - match layout_type { - DatabaseLayout::Grid => {}, - DatabaseLayout::Board => {}, - DatabaseLayout::Calendar => { - if layout_setting.calendar.is_none() { - let layout_setting = CalendarLayoutSetting::new(date_field_id.clone()); - self - .v_set_layout_settings(LayoutSettingParams { - layout_type, - calendar: Some(layout_setting), - }) - .await?; - } - }, + let resolver = DatabaseLayoutDepsResolver::new(self.delegate.get_database(), new_layout_type); + resolver.resolve_deps_when_update_layout_type(&self.view_id); } + // initialize the group controller if the current layout support grouping + *self.group_controller.write().await = + new_group_controller(self.view_id.clone(), self.delegate.clone()).await?; + let payload = DatabaseLayoutMetaPB { view_id: self.view_id.clone(), - layout: layout_type.into(), + layout: new_layout_type.into(), }; send_notification(&self.view_id, DatabaseNotification::DidUpdateDatabaseLayout) .payload(payload) diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs index 0fdd4651fb..c0dd434a03 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -94,7 +94,7 @@ impl DatabaseViews { // If the id of the grouping field is equal to the updated field's id, then we need to // update the group setting if view_editor.is_grouping_field(field_id).await { - view_editor.v_update_grouping_field(field_id).await?; + view_editor.v_grouping_by_field(field_id).await?; } view_editor .v_did_update_field_type_option(field_id, old_field) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 1bc3f3b48a..a2a86f05b8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -1,18 +1,21 @@ +use std::collections::HashMap; +use std::fmt::Formatter; +use std::marker::PhantomData; +use std::sync::Arc; + +use collab_database::fields::Field; +use indexmap::IndexMap; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_infra::future::Fut; + use crate::entities::{GroupChangesPB, GroupPB, InsertedGroupPB}; use crate::services::field::RowSingleCellData; use crate::services::group::{ default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting, }; -use collab_database::fields::Field; -use flowy_error::{FlowyError, FlowyResult}; -use indexmap::IndexMap; -use lib_infra::future::Fut; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::collections::HashMap; -use std::fmt::Formatter; -use std::marker::PhantomData; -use std::sync::Arc; pub trait GroupSettingReader: Send + Sync + 'static { fn get_group_setting(&self, view_id: &str) -> Fut>>; @@ -361,10 +364,10 @@ where })?; if let Some(group) = update_group { - self.group_by_id.get_mut(&group.id).map(|group_data| { + if let Some(group_data) = self.group_by_id.get_mut(&group.id) { group_data.name = group.name.clone(); group_data.is_visible = group.visible; - }); + }; } Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs index 65d1827a0e..e300add987 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs @@ -1,9 +1,10 @@ -use crate::entities::FieldType; -use crate::services::cell::stringify_cell_data; use collab_database::database::Database; +use indexmap::IndexMap; use flowy_error::{FlowyError, FlowyResult}; -use indexmap::IndexMap; + +use crate::entities::FieldType; +use crate::services::cell::stringify_cell_data; #[derive(Debug, Clone, Copy)] pub enum CSVFormat { @@ -20,7 +21,7 @@ impl CSVExport { pub fn export_database(&self, database: &Database, style: CSVFormat) -> FlowyResult { let mut wtr = csv::Writer::from_writer(vec![]); let inline_view_id = database.get_inline_view_id(); - let fields = database.get_fields(&inline_view_id, None); + let fields = database.get_fields_in_view(&inline_view_id, None); // Write fields let field_records = fields diff --git a/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs b/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs index db9318bc18..e7bc4e98ef 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs @@ -1,10 +1,13 @@ -use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser}; +use std::collections::HashMap; +use std::sync::Arc; + use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType}; + use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE; use flowy_document2::document_data::default_document_data; use flowy_document2::manager::DocumentManager; -use std::collections::HashMap; -use std::sync::Arc; + +use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser}; #[tokio::test] async fn undo_redo_test() { @@ -38,22 +41,22 @@ async fn undo_redo_test() { action: BlockActionType::Insert, payload: BlockActionPayload { block: text_block, - parent_id: Some(page_id.clone()), + parent_id: Some(page_id), prev_id: None, }, }; document.apply_action(vec![insert_text_action]); let can_undo = document.can_undo(); - assert_eq!(can_undo, true); + assert!(can_undo); // undo the insert let undo = document.undo(); - assert_eq!(undo, true); + assert!(undo); assert_eq!(document.get_block(&text_block_id), None); let can_redo = document.can_redo(); assert!(can_redo); // redo the insert let redo = document.redo(); - assert_eq!(redo, true); + assert!(redo); } diff --git a/frontend/rust-lib/flowy-folder2/src/entities/view.rs b/frontend/rust-lib/flowy-folder2/src/entities/view.rs index 15ef8ed158..edd6ba1ee5 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/view.rs @@ -10,6 +10,21 @@ use flowy_error::ErrorCode; use crate::entities::parser::view::{ViewDesc, ViewIdentify, ViewName, ViewThumbnail}; use crate::view_operation::gen_view_id; +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct ChildViewUpdatePB { + #[pb(index = 1)] + pub parent_view_id: String, + + #[pb(index = 2)] + pub create_child_views: Vec, + + #[pb(index = 3)] + pub delete_child_views: Vec, + + #[pb(index = 4)] + pub update_child_views: Vec, +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct ViewPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index d9f238ad2d..12f79e4226 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -18,8 +18,9 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use crate::deps::{FolderCloudService, FolderUser}; use crate::entities::{ - view_pb_with_child_views, CreateViewParams, CreateWorkspaceParams, DeletedViewPB, - RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, ViewPB, WorkspacePB, + view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams, + CreateWorkspaceParams, DeletedViewPB, RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, + UpdateViewParams, ViewPB, WorkspacePB, }; use crate::notification::{ send_notification, send_workspace_notification, send_workspace_setting_notification, @@ -257,7 +258,6 @@ impl Folder2Manager { folder.insert_view(view.clone()); }); - notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]); Ok(view) } @@ -332,6 +332,7 @@ impl Folder2Manager { #[tracing::instrument(level = "debug", skip(self), err)] pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> { self.with_folder((), |folder| { + let view = folder.views.get_view(view_id); folder.add_trash(vec![view_id.to_string()]); // notify the parent view that the view is moved to trash @@ -341,6 +342,13 @@ impl Folder2Manager { index: None, }) .send(); + + if let Some(view) = view { + notify_child_views_changed( + view_pb_without_child_views(view), + ChildViewChangeReason::DidDeleteView, + ); + } }); Ok(()) @@ -415,6 +423,7 @@ impl Folder2Manager { .set_cover_url_if_not_none(params.cover_url) .done() }); + Some((old_view, new_view)) }); @@ -632,10 +641,25 @@ fn listen_on_view_change(mut rx: ViewChangeReceiver, weak_mutex_folder: &Weak { + notify_child_views_changed( + view_pb_without_child_views(view.clone()), + ChildViewChangeReason::DidCreateView, + ); notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]); }, - ViewChange::DidDeleteView { views: _ } => {}, + ViewChange::DidDeleteView { views } => { + for view in views { + notify_child_views_changed( + view_pb_without_child_views(view), + ChildViewChangeReason::DidDeleteView, + ); + } + }, ViewChange::DidUpdate { view } => { + notify_child_views_changed( + view_pb_without_child_views(view.clone()), + ChildViewChangeReason::DidUpdateView, + ); notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]); }, }; @@ -764,7 +788,7 @@ fn notify_parent_view_did_change>( // Post the notification let parent_view_pb = view_pb_with_child_views(parent_view, child_views); - send_notification(parent_view_id, FolderNotification::DidUpdateChildViews) + send_notification(parent_view_id, FolderNotification::DidUpdateView) .payload(parent_view_pb) .send(); } @@ -773,6 +797,38 @@ fn notify_parent_view_did_change>( None } +pub enum ChildViewChangeReason { + DidCreateView, + DidDeleteView, + DidUpdateView, +} + +/// Notify the the list of parent view ids that its child views were changed. +#[tracing::instrument(level = "debug", skip_all)] +fn notify_child_views_changed(view_pb: ViewPB, reason: ChildViewChangeReason) { + let parent_view_id = view_pb.parent_view_id.clone(); + let mut payload = ChildViewUpdatePB { + parent_view_id: view_pb.parent_view_id.clone(), + ..Default::default() + }; + + match reason { + ChildViewChangeReason::DidCreateView => { + payload.create_child_views.push(view_pb); + }, + ChildViewChangeReason::DidDeleteView => { + payload.delete_child_views.push(view_pb.id); + }, + ChildViewChangeReason::DidUpdateView => { + payload.update_child_views.push(view_pb); + }, + } + + send_notification(&parent_view_id, FolderNotification::DidUpdateChildViews) + .payload(payload) + .send(); +} + fn folder_not_init_error() -> FlowyError { FlowyError::internal().context("Folder not initialized") } diff --git a/frontend/rust-lib/flowy-folder2/src/notification.rs b/frontend/rust-lib/flowy-folder2/src/notification.rs index 5495f93119..88c452b78c 100644 --- a/frontend/rust-lib/flowy-folder2/src/notification.rs +++ b/frontend/rust-lib/flowy-folder2/src/notification.rs @@ -1,9 +1,11 @@ -use crate::entities::{view_pb_without_child_views, WorkspacePB, WorkspaceSettingPB}; use collab_folder::core::{View, Workspace}; + use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; use lib_dispatch::prelude::ToBytes; +use crate::entities::{view_pb_without_child_views, WorkspacePB, WorkspaceSettingPB}; + const OBSERVABLE_CATEGORY: &str = "Workspace"; #[derive(ProtoBuf_Enum, Debug, Default)] @@ -18,9 +20,7 @@ pub(crate) enum FolderNotification { DidUpdateWorkspaceViews = 3, /// Trigger when the settings of the workspace are changed. The changes including the latest visiting view, etc DidUpdateWorkspaceSetting = 4, - DidUpdateView = 29, - /// Trigger when the properties including rename,update description of the view are changed DidUpdateChildViews = 30, /// Trigger after deleting the view DidDeleteView = 31, diff --git a/frontend/rust-lib/flowy-test/tests/document/utils.rs b/frontend/rust-lib/flowy-test/tests/document/utils.rs index d6bc95a4d7..3c7adc4646 100644 --- a/frontend/rust-lib/flowy-test/tests/document/utils.rs +++ b/frontend/rust-lib/flowy-test/tests/document/utils.rs @@ -41,7 +41,7 @@ pub fn gen_insert_block_action(document: OpenDocumentData) -> BlockActionPB { let data = block.data.clone(); let new_block_id = gen_id(); let new_block = BlockPB { - id: new_block_id.clone(), + id: new_block_id, ty: block.ty.clone(), data, parent_id: page_id.clone(),