diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart new file mode 100644 index 0000000000..f2a0f15c8d --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_cell_test.dart @@ -0,0 +1,185 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.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('grid cell', () { + 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('edit text cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + await tester.editCell( + rowIndex: 0, + fieldType: FieldType.RichText, + input: 'hello world', + ); + + await tester.assertCellContent( + rowIndex: 0, + fieldType: FieldType.RichText, + content: 'hello world', + ); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit number cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.Number; + + // Create a number field + await tester.createField(fieldType, fieldType.name); + + await tester.editCell( + rowIndex: 0, + fieldType: fieldType, + input: '-1', + ); + // edit the next cell to force the previous cell at row 0 to lose focus + await tester.editCell( + rowIndex: 1, + fieldType: fieldType, + input: '0.2', + ); + // -1 -> -1 + await tester.assertCellContent( + rowIndex: 0, + fieldType: fieldType, + content: '-1', + ); + + // edit the next cell to force the previous cell at row 1 to lose focus + await tester.editCell( + rowIndex: 2, + fieldType: fieldType, + input: '.1', + ); + // 0.2 -> 0.2 + await tester.assertCellContent( + rowIndex: 1, + fieldType: fieldType, + content: '0.2', + ); + + // edit the next cell to force the previous cell at row 2 to lose focus + await tester.editCell( + rowIndex: 0, + fieldType: fieldType, + input: '', + ); + // .1 -> 0.1 + await tester.assertCellContent( + rowIndex: 2, + fieldType: fieldType, + content: '0.1', + ); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit checkbox cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + await tester.assertCheckboxCell(rowIndex: 0, isSelected: false); + await tester.tapCheckboxCellInGrid(rowIndex: 0); + await tester.assertCheckboxCell(rowIndex: 0, isSelected: true); + + await tester.tapCheckboxCellInGrid(rowIndex: 1); + await tester.tapCheckboxCellInGrid(rowIndex: 2); + await tester.assertCheckboxCell(rowIndex: 1, isSelected: true); + await tester.assertCheckboxCell(rowIndex: 2, isSelected: true); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit create time cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.CreatedTime; + // Create a create time field + // The create time field is not editable + await tester.createField(fieldType, fieldType.name); + + await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); + + await tester.findDateEditor(findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit last time cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.LastEditedTime; + // Create a last time field + // The last time field is not editable + await tester.createField(fieldType, fieldType.name); + + await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); + + await tester.findDateEditor(findsNothing); + + await tester.pumpAndSettle(); + }); + + testWidgets('edit time cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + const fieldType = FieldType.DateTime; + await tester.createField(fieldType, fieldType.name); + + // Tap the cell to invoke the field editor + await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); + await tester.findDateEditor(findsOneWidget); + + // Select the date + await tester.selectDay(content: 3); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_field_test.dart b/frontend/appflowy_flutter/integration_test/database_field_test.dart new file mode 100644 index 0000000000..4e9a5446ed --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_field_test.dart @@ -0,0 +1,202 @@ +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.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('grid page', () { + 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('rename existing field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Invoke the field editor + await tester.tapGridFieldWithName('Name'); + await tester.tapEditPropertyButton(); + + await tester.renameField('hello world'); + await tester.dismissFieldEditor(); + + await tester.tapGridFieldWithName('hello world'); + await tester.pumpAndSettle(); + }); + + testWidgets('update field type of existing field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Invoke the field editor + await tester.tapGridFieldWithName('Type'); + await tester.tapEditPropertyButton(); + + await tester.tapTypeOptionButton(); + await tester.selectFieldType(FieldType.Checkbox); + await tester.dismissFieldEditor(); + + await tester.assertFieldTypeWithFieldName( + 'Type', + FieldType.Checkbox, + ); + await tester.pumpAndSettle(); + }); + + testWidgets('create a field and rename it', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.createField(FieldType.Checklist, 'checklist'); + + // check the field is created successfully + await tester.findFieldWithName('checklist'); + await tester.pumpAndSettle(); + }); + + testWidgets('delete field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.createField(FieldType.Checkbox, 'New field 1'); + + // Delete the field + await tester.tapGridFieldWithName('New field 1'); + await tester.tapDeletePropertyButton(); + + // confirm delete + await tester.tapDialogOkButton(); + + await tester.noFieldWithName('New field 1'); + await tester.pumpAndSettle(); + }); + + testWidgets('duplicate field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField('New field 1'); + await tester.dismissFieldEditor(); + await tester.createField(FieldType.RichText, 'New field 1'); + + // Delete the field + await tester.tapGridFieldWithName('New field 1'); + await tester.tapDuplicatePropertyButton(); + + await tester.findFieldWithName('New field 1 (copy)'); + await tester.pumpAndSettle(); + }); + + testWidgets('hide field', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // create a field + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField('New field 1'); + await tester.dismissFieldEditor(); + + // Delete the field + await tester.tapGridFieldWithName('New field 1'); + await tester.tapHidePropertyButton(); + + await tester.noFieldWithName('New field 1'); + await tester.pumpAndSettle(); + }); + + testWidgets('create checklist field ', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + + // Open the type option menu + await tester.tapTypeOptionButton(); + + await tester.selectFieldType(FieldType.Checklist); + + // After update the field type, the cells should be updated + await tester.findCellByFieldType(FieldType.Checklist); + + await tester.pumpAndSettle(); + }); + + testWidgets('create list of fields', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + for (final fieldType in [ + FieldType.Checklist, + FieldType.DateTime, + FieldType.Number, + FieldType.URL, + FieldType.MultiSelect, + FieldType.LastEditedTime, + FieldType.CreatedTime, + FieldType.Checkbox, + ]) { + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField(fieldType.name); + + // Open the type option menu + await tester.tapTypeOptionButton(); + + await tester.selectFieldType(fieldType); + await tester.dismissFieldEditor(); + + // After update the field type, the cells should be updated + await tester.findCellByFieldType(fieldType); + await tester.pumpAndSettle(); + } + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart new file mode 100644 index 0000000000..a424225ec7 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart @@ -0,0 +1,241 @@ +import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/database_test_op.dart'; +import 'util/ime.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + 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('open first row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + }); + + testWidgets('insert emoji in the row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.hoverRowBanner(); + + await tester.openEmojiPicker(); + await tester.switchToEmojiList(); + await tester.tapEmoji('😀'); + + // After select the emoji, the EmojiButton will show up + await tester.tapButton(find.byType(EmojiButton)); + }); + + testWidgets('update emoji in the row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + await tester.hoverRowBanner(); + await tester.openEmojiPicker(); + await tester.switchToEmojiList(); + await tester.tapEmoji('😀'); + + // Update existing selected emoji + await tester.tapButton(find.byType(EmojiButton)); + await tester.switchToEmojiList(); + await tester.tapEmoji('😅'); + + // The emoji already displayed in the row banner + final emojiText = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == '😅', + ); + + // The number of emoji should be two. One in the row displayed in the grid + // one in the row detail page. + expect(emojiText, findsNWidgets(2)); + }); + + testWidgets('remove emoji in the row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + await tester.hoverRowBanner(); + await tester.openEmojiPicker(); + await tester.switchToEmojiList(); + await tester.tapEmoji('😀'); + + // Remove the emoji + await tester.tapButton(find.byType(RemoveEmojiButton)); + final emojiText = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == '😀', + ); + expect(emojiText, findsNothing); + }); + + testWidgets('create list of fields in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + for (final fieldType in [ + FieldType.Checklist, + FieldType.DateTime, + FieldType.Number, + FieldType.URL, + FieldType.MultiSelect, + FieldType.LastEditedTime, + FieldType.CreatedTime, + FieldType.Checkbox, + ]) { + await tester.tapRowDetailPageCreatePropertyButton(); + await tester.renameField(fieldType.name); + + // Open the type option menu + await tester.tapTypeOptionButton(); + + await tester.selectFieldType(fieldType); + await tester.dismissFieldEditor(); + + // After update the field type, the cells should be updated + await tester.findCellByFieldType(fieldType); + await tester.scrollRowDetailByOffset(const Offset(0, -50)); + } + }); + + testWidgets('check document is exist in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + // Each row detail page should have a document + await tester.assertDocumentExistInRowDetailPage(); + }); + + testWidgets('update the content of the document and re-open it', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + // Wait for the document to be loaded + await tester.wait(500); + + // Focus on the editor + final textBlock = find.byType(TextBlockComponentWidget); + await tester.tapAt(tester.getCenter(textBlock)); + + // Input some text + const inputText = 'Hello world'; + await tester.ime.insertText(inputText); + expect( + find.textContaining(inputText, findRichText: true), + findsOneWidget, + ); + + // Tap outside to dismiss the field + await tester.tapAt(Offset.zero); + await tester.pumpAndSettle(); + + // Re-open the document + await tester.openFirstRowDetailPage(); + expect( + find.textContaining(inputText, findRichText: true), + findsOneWidget, + ); + }); + + testWidgets('delete row in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.tapRowDetailPageDeleteRowButton(); + await tester.tapEscButton(); + + await tester.assertNumberOfRowsInGridPage(2); + }); + + testWidgets('duplicate row in row detail page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // Create a new grid + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.tapRowDetailPageDuplicateRowButton(); + await tester.tapEscButton(); + + await tester.assertNumberOfRowsInGridPage(4); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_row_test.dart b/frontend/appflowy_flutter/integration_test/database_row_test.dart new file mode 100644 index 0000000000..14a336a89f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_row_test.dart @@ -0,0 +1,85 @@ +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('grid', () { + 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 row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.tapCreateRowButtonInGrid(); + + // The initial number of rows is 3 + await tester.assertNumberOfRowsInGridPage(4); + await tester.pumpAndSettle(); + }); + + testWidgets('create row from row menu of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.hoverOnFirstRowOfGrid(); + + await tester.tapCreateRowButtonInRowMenuOfGrid(); + + // The initial number of rows is 3 + await tester.assertNumberOfRowsInGridPage(4); + await tester.assertRowCountInGridPage(4); + await tester.pumpAndSettle(); + }); + + testWidgets('delete row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.hoverOnFirstRowOfGrid(); + + // Open the row menu and then click the delete + await tester.tapRowMenuButtonInGrid(); + await tester.tapDeleteOnRowMenu(); + + // The initial number of rows is 3 + await tester.assertNumberOfRowsInGridPage(2); + await tester.assertRowCountInGridPage(2); + await tester.pumpAndSettle(); + }); + + testWidgets('check number of row indicator in the initial grid', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + await tester.assertRowCountInGridPage(3); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/database_setting_test.dart b/frontend/appflowy_flutter/integration_test/database_setting_test.dart new file mode 100644 index 0000000000..ba4e88ea9f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/database_setting_test.dart @@ -0,0 +1,66 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.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('grid', () { + 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 layout', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // open setting + await tester.tapDatabaseSettingButton(); + // select the layout + await tester.tapDatabaseLayoutButton(); + // select layout by board + await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board); + + await tester.pumpAndSettle(); + }); + + testWidgets('update layout multiple times', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.tapAddButton(); + await tester.tapCreateGridButton(); + + // 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.Calendar); + await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 46123f3897..c640e8512d 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; + import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart'; @@ -39,6 +40,13 @@ extension CommonOperations on WidgetTester { await tapButtonWithName(LocaleKeys.document_menuName.tr()); } + /// Tap the create grid button. + /// + /// Must call [tapAddButton] first. + Future tapCreateGridButton() async { + await tapButtonWithName(LocaleKeys.grid_menuName.tr()); + } + /// Tap the import button. /// /// Must call [tapAddButton] first. @@ -105,12 +113,14 @@ extension CommonOperations on WidgetTester { Finder finder, { Offset? offset, }) async { - final gesture = await createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(gesture.removePointer); - await pump(); - await gesture.moveTo(offset ?? getCenter(finder)); - await pumpAndSettle(); + try { + final gesture = await createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await pump(); + await gesture.moveTo(offset ?? getCenter(finder)); + await pumpAndSettle(); + } catch (_) {} } /// Hover on 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 new file mode 100644 index 0000000000..63b84fccdb --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -0,0 +1,481 @@ +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/grid/presentation/widgets/header/field_cell_action_sheet.dart'; +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/grid_layout.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/database_setting.dart'; +import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/footer/grid_footer.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import 'base.dart'; +import 'common_operations.dart'; + +extension AppFlowyDatabaseTest on WidgetTester { + Future hoverOnFirstRowOfGrid() async { + final findRow = find.byType(GridRow); + expect(findRow, findsWidgets); + + final firstRow = findRow.first; + await hoverOnWidget(firstRow); + } + + Future editCell({ + required int rowIndex, + required FieldType fieldType, + required String input, + }) async { + final findRow = find.byType(GridRow); + final findCell = finderForFieldType(fieldType); + + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + ); + + expect(cell, findsOneWidget); + await enterText(cell, input); + await pumpAndSettle(); + } + + Future tapCheckboxCellInGrid({ + required int rowIndex, + }) async { + final findRow = find.byType(GridRow); + final findCell = finderForFieldType(FieldType.Checkbox); + + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + ); + + final button = find.descendant( + of: cell, + matching: find.byType(FlowyIconButton), + ); + + expect(cell, findsOneWidget); + await tapButton(button); + } + + Future assertCheckboxCell({ + required int rowIndex, + required bool isSelected, + }) async { + final findRow = find.byType(GridRow); + final findCell = finderForFieldType(FieldType.Checkbox); + + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + ); + + var finder = find.byType(CheckboxCellUncheck); + if (isSelected) { + finder = find.byType(CheckboxCellCheck); + } + + expect( + find.descendant( + of: cell, + matching: finder, + ), + findsOneWidget, + ); + } + + Future tapCellInGrid({ + required int rowIndex, + required FieldType fieldType, + }) async { + final findRow = find.byType(GridRow); + final findCell = finderForFieldType(fieldType); + + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + ); + + expect(cell, findsOneWidget); + await tapButton(cell); + } + + Future assertCellContent({ + required int rowIndex, + required FieldType fieldType, + required String content, + }) async { + final findRow = find.byType(GridRow); + final findCell = finderForFieldType(fieldType); + final cell = find.descendant( + of: findRow.at(rowIndex), + matching: findCell, + ); + + final findContent = find.descendant( + of: cell, + matching: find.text(content), + ); + + expect(findContent, findsOneWidget); + } + + Future selectDay({ + required int content, + }) async { + final findCalendar = find.byType(TableCalendar); + final findDay = find.text(content.toString()); + + final finder = find.descendant( + of: findCalendar, + matching: findDay, + ); + + await tapButton(finder); + } + + Future openFirstRowDetailPage() async { + await hoverOnFirstRowOfGrid(); + + final expandButton = find.byType(PrimaryCellAccessory); + expect(expandButton, findsOneWidget); + await tapButton(expandButton); + } + + Future hoverRowBanner() async { + final banner = find.byType(RowBanner); + expect(banner, findsOneWidget); + + await startGesture( + getTopLeft(banner), + kind: PointerDeviceKind.mouse, + ); + + await pumpAndSettle(); + } + + Future openEmojiPicker() async { + await tapButton(find.byType(EmojiPickerButton)); + await tapButton(find.byType(EmojiSelectionMenu)); + } + + /// Must call [openEmojiPicker] first + Future switchToEmojiList() async { + final icon = find.byIcon(Icons.tag_faces); + await tapButton(icon); + } + + Future tapEmoji(String emoji) async { + final emojiWidget = find.text(emoji); + await tapButton(emojiWidget); + } + + Future scrollGridByOffset(Offset offset) async { + await drag(find.byType(GridPage), offset); + await pumpAndSettle(); + } + + Future scrollRowDetailByOffset(Offset offset) async { + await drag(find.byType(RowDetailPage), offset); + await pumpAndSettle(); + } + + Future scrollToRight(Finder find) async { + final size = getSize(find); + await drag(find, Offset(-size.width, 0)); + await pumpAndSettle(const Duration(milliseconds: 500)); + } + + Future tapNewPropertyButton() async { + await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr()); + await pumpAndSettle(); + } + + Future tapGridFieldWithName(String name) async { + final field = find.byWidgetPredicate( + (widget) => widget is FieldCellButton && widget.field.name == name, + ); + await tapButton(field); + await pumpAndSettle(); + } + + /// Should call [tapGridFieldWithName] first. + Future tapEditPropertyButton() async { + await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr()); + await pumpAndSettle(const Duration(milliseconds: 200)); + } + + /// Should call [tapGridFieldWithName] first. + Future tapDeletePropertyButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldActionCell && widget.action == FieldAction.delete, + ); + await tapButton(field); + } + + /// Should call [tapGridFieldWithName] first. + Future tapDialogOkButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is PrimaryTextButton && + widget.label == LocaleKeys.button_OK.tr(), + ); + await tapButton(field); + } + + /// Should call [tapGridFieldWithName] first. + Future tapDuplicatePropertyButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldActionCell && widget.action == FieldAction.duplicate, + ); + await tapButton(field); + } + + /// Should call [tapGridFieldWithName] first. + Future tapHidePropertyButton() async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldActionCell && widget.action == FieldAction.hide, + ); + await tapButton(field); + } + + Future tapRowDetailPageCreatePropertyButton() async { + await tapButton(find.byType(CreateRowFieldButton)); + } + + Future tapRowDetailPageDeleteRowButton() async { + await tapButton(find.byType(RowDetailPageDeleteButton)); + } + + Future tapRowDetailPageDuplicateRowButton() async { + await tapButton(find.byType(RowDetailPageDuplicateButton)); + } + + Future tapTypeOptionButton() async { + await tapButton(find.byType(SwitchFieldButton)); + } + + Future tapEscButton() async { + await sendKeyEvent(LogicalKeyboardKey.escape); + } + + /// Must call [tapTypeOptionButton] first. + Future selectFieldType(FieldType fieldType) async { + final fieldTypeButton = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == fieldType.title(), + ); + await tapButton(fieldTypeButton); + } + + /// Each field has its own cell, so we can find the corresponding cell by + /// the field type after create a new field. + Future findCellByFieldType(FieldType fieldType) async { + final finder = finderForFieldType(fieldType); + expect(finder, findsWidgets); + } + + Future assertNumberOfFieldsInGridPage(int num) async { + expect(find.byType(GridFieldCell), findsNWidgets(num)); + } + + Future assertNumberOfRowsInGridPage(int num) async { + expect(find.byType(GridRow), findsNWidgets(num)); + } + + Future assertDocumentExistInRowDetailPage() async { + expect(find.byType(RowDocument), findsOneWidget); + } + + /// Check the field type of the [FieldCellButton] is the same as the name. + Future assertFieldTypeWithFieldName( + String name, + FieldType fieldType, + ) async { + final field = find.byWidgetPredicate( + (widget) => + widget is FieldCellButton && + widget.field.fieldType == fieldType && + widget.field.name == name, + ); + + expect(field, findsOneWidget); + } + + Future findFieldWithName(String name) async { + final field = find.byWidgetPredicate( + (widget) => widget is FieldCellButton && widget.field.name == name, + ); + expect(field, findsOneWidget); + } + + Future noFieldWithName(String name) async { + final field = find.byWidgetPredicate( + (widget) => widget is FieldCellButton && widget.field.name == name, + ); + expect(field, findsNothing); + } + + Future renameField(String newName) async { + final textField = find.byType(FieldNameTextField); + expect(textField, findsOneWidget); + await enterText(textField, newName); + await pumpAndSettle(); + } + + Future dismissFieldEditor() async { + await sendKeyEvent(LogicalKeyboardKey.escape); + await sendKeyEvent(LogicalKeyboardKey.escape); + await sendKeyEvent(LogicalKeyboardKey.escape); + await pumpAndSettle(); + } + + Future findFieldEditor(dynamic matcher) async { + final finder = find.byType(FieldEditor); + expect(finder, matcher); + } + + Future findDateEditor(dynamic matcher) async { + final finder = find.byType(DateCellEditor); + expect(finder, matcher); + } + + Future tapCreateRowButtonInGrid() async { + await tapButton(find.byType(GridAddRowButton)); + } + + Future tapCreateRowButtonInRowMenuOfGrid() async { + await tapButton(find.byType(InsertRowButton)); + } + + Future tapRowMenuButtonInGrid() async { + await tapButton(find.byType(RowMenuButton)); + } + + /// Should call [tapRowMenuButtonInGrid] first. + Future tapDeleteOnRowMenu() async { + await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); + } + + Future assertRowCountInGridPage(int num) async { + final text = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == rowCountString(num), + ); + expect(text, findsOneWidget); + } + + Future createField(FieldType fieldType, String name) async { + await scrollToRight(find.byType(GridPage)); + await tapNewPropertyButton(); + await renameField(name); + await tapTypeOptionButton(); + await selectFieldType(fieldType); + await dismissFieldEditor(); + } + + Future tapDatabaseSettingButton() async { + await tapButton(find.byType(SettingButton)); + } + + /// Should call [tapDatabaseSettingButton] first. + Future tapDatabaseLayoutButton() async { + final findSettingItem = find.byType(DatabaseSettingItem); + final findLayoutButton = find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.title == DatabaseSettingAction.showLayout.title(), + ); + + final button = find.descendant( + of: findSettingItem, + matching: findLayoutButton, + ); + + await tapButton(button); + } + + Future selectDatabaseLayoutType(DatabaseLayoutPB layout) async { + final findLayoutCell = find.byType(DatabaseViewLayoutCell); + final findText = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == layout.layoutName(), + ); + + final button = find.descendant( + of: findLayoutCell, + matching: findText, + ); + + await tapButton(button); + } + + Future assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async { + expect(finderForDatabaseLayoutType(layout), findsOneWidget); + } +} + +Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) { + switch (layout) { + case DatabaseLayoutPB.Board: + return find.byType(BoardPage); + case DatabaseLayoutPB.Calendar: + return find.byType(CalendarPage); + case DatabaseLayoutPB.Grid: + return find.byType(GridPage); + default: + throw Exception('Unknown database layout type: $layout'); + } +} + +Finder finderForFieldType(FieldType fieldType) { + switch (fieldType) { + case FieldType.Checkbox: + return find.byType(GridCheckboxCell); + case FieldType.DateTime: + return find.byType(GridDateCell); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return find.byType(GridDateCell); + case FieldType.SingleSelect: + return find.byType(GridSingleSelectCell); + case FieldType.MultiSelect: + return find.byType(GridMultiSelectCell); + case FieldType.Checklist: + return find.byType(GridChecklistCell); + case FieldType.Number: + return find.byType(GridNumberCell); + case FieldType.RichText: + return find.byType(GridTextCell); + case FieldType.URL: + return find.byType(GridURLCell); + default: + throw Exception('Unknown field type: $fieldType'); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/ime.dart b/frontend/appflowy_flutter/integration_test/util/ime.dart new file mode 100644 index 0000000000..30b1388e0d --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/ime.dart @@ -0,0 +1,54 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension IME on WidgetTester { + IMESimulator get ime => IMESimulator(this); +} + +class IMESimulator { + IMESimulator(this.tester) { + client = findDeltaTextInputClient(); + } + + final WidgetTester tester; + late final DeltaTextInputClient client; + + Future insertText(String text) async { + for (final c in text.characters) { + await insertCharacter(c); + } + } + + Future insertCharacter(String character) async { + final value = client.currentTextEditingValue; + if (value == null) { + assert(false); + return; + } + final deltas = [ + TextEditingDeltaInsertion( + textInserted: character, + oldText: value.text.replaceRange( + value.selection.start, + value.selection.end, + '', + ), + insertionOffset: value.selection.baseOffset, + selection: TextSelection.collapsed( + offset: value.selection.baseOffset + 1, + ), + composing: TextRange.empty, + ), + ]; + client.updateEditingValueWithDeltas(deltas); + await tester.pumpAndSettle(); + } + + DeltaTextInputClient findDeltaTextInputClient() { + final finder = find.byType(KeyboardServiceWidget); + final KeyboardServiceWidgetState state = tester.state(finder); + return state.textInputService as DeltaTextInputClient; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart index cac1857281..586734a96e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/field/field_listener.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_meta_listener.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; @@ -22,39 +23,45 @@ import 'cell_service.dart'; /// // ignore: must_be_immutable class CellController extends Equatable { - final DatabaseCellContext cellContext; + DatabaseCellContext _cellContext; final CellCache _cellCache; final CellCacheKey _cacheKey; final FieldBackendService _fieldBackendSvc; - final SingleFieldListener _fieldListener; final CellDataLoader _cellDataLoader; final CellDataPersistence _cellDataPersistence; CellListener? _cellListener; + RowMetaListener? _rowMetaListener; + SingleFieldListener? _fieldListener; CellDataNotifier? _cellDataNotifier; VoidCallback? _onCellFieldChanged; + VoidCallback? _onRowMetaChanged; Timer? _loadDataOperation; Timer? _saveDataOperation; - String get viewId => cellContext.viewId; + String get viewId => _cellContext.viewId; - RowId get rowId => cellContext.rowId; + RowId get rowId => _cellContext.rowId; - String get fieldId => cellContext.fieldInfo.id; + String get fieldId => _cellContext.fieldInfo.id; - FieldInfo get fieldInfo => cellContext.fieldInfo; + FieldInfo get fieldInfo => _cellContext.fieldInfo; - FieldType get fieldType => cellContext.fieldInfo.fieldType; + FieldType get fieldType => _cellContext.fieldInfo.fieldType; + + String? get emoji => _cellContext.emoji; CellController({ - required this.cellContext, + required DatabaseCellContext cellContext, required CellCache cellCache, required CellDataLoader cellDataLoader, required CellDataPersistence cellDataPersistence, - }) : _cellCache = cellCache, + }) : _cellContext = cellContext, + _cellCache = cellCache, _cellDataLoader = cellDataLoader, _cellDataPersistence = cellDataPersistence, + _rowMetaListener = RowMetaListener(cellContext.rowId), _fieldListener = SingleFieldListener(fieldId: cellContext.fieldId), _fieldBackendSvc = FieldBackendService( viewId: cellContext.viewId, @@ -84,20 +91,22 @@ class CellController extends Equatable { ); /// 2.Listen on the field event and load the cell data if needed. - _fieldListener.start( - onFieldChanged: (result) { - result.fold( - (fieldPB) { - /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed - /// For example: - /// ï¿¥12 -> $12 - if (_cellDataLoader.reloadOnFieldChanged) { - _loadData(); - } - _onCellFieldChanged?.call(); - }, - (err) => Log.error(err), - ); + _fieldListener?.start( + onFieldChanged: (fieldPB) { + /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed + /// For example: + /// ï¿¥12 -> $12 + if (_cellDataLoader.reloadOnFieldChanged) { + _loadData(); + } + _onCellFieldChanged?.call(); + }, + ); + + _rowMetaListener?.start( + callback: (newRowMeta) { + _cellContext = _cellContext.copyWith(rowMeta: newRowMeta); + _onRowMetaChanged?.call(); }, ); } @@ -105,9 +114,11 @@ class CellController extends Equatable { /// Listen on the cell content or field changes VoidCallback? startListening({ required void Function(T?) onCellChanged, + VoidCallback? onRowMetaChanged, VoidCallback? onCellFieldChanged, }) { _onCellFieldChanged = onCellFieldChanged; + _onRowMetaChanged = onRowMetaChanged; /// Notify the listener, the cell data was changed. onCellChangedFn() => onCellChanged(_cellDataNotifier?.value); @@ -186,18 +197,26 @@ class CellController extends Equatable { } Future dispose() async { + await _rowMetaListener?.stop(); + _rowMetaListener = null; + await _cellListener?.stop(); + _cellListener = null; + + await _fieldListener?.stop(); + _fieldListener = null; + _loadDataOperation?.cancel(); _saveDataOperation?.cancel(); _cellDataNotifier?.dispose(); - await _fieldListener.stop(); _cellDataNotifier = null; + _onRowMetaChanged = null; } @override List get props => [ _cellCache.get(_cacheKey) ?? "", - cellContext.rowId + cellContext.fieldInfo.id + _cellContext.rowId + _cellContext.fieldInfo.id ]; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart index 501fa8c2b1..31e46a5189 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:dartz/dartz.dart'; @@ -52,18 +53,23 @@ class CellBackendService { class DatabaseCellContext with _$DatabaseCellContext { const factory DatabaseCellContext({ required String viewId, - required RowId rowId, + required RowMetaPB rowMeta, required FieldInfo fieldInfo, }) = _DatabaseCellContext; // ignore: unused_element const DatabaseCellContext._(); + String get rowId => rowMeta.id; + String get fieldId => fieldInfo.id; FieldType get fieldType => fieldInfo.fieldType; ValueKey key() { - return ValueKey("$rowId$fieldId${fieldInfo.fieldType}"); + return ValueKey("${rowMeta.id}$fieldId${fieldInfo.fieldType}"); } + + /// Only the primary field can have an emoji. + String? get emoji => fieldInfo.isPrimary ? rowMeta.icon : null; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart index d836d188be..9b38fc1b0d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; @@ -7,17 +6,23 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:dartz/dartz.dart'; class ChecklistCellBackendService { - final DatabaseCellContext cellContext; + final String viewId; + final String fieldId; + final String rowId; - ChecklistCellBackendService({required this.cellContext}); + ChecklistCellBackendService({ + required this.viewId, + required this.fieldId, + required this.rowId, + }); Future> create({ required String name, }) { final payload = ChecklistCellDataChangesetPB.create() - ..viewId = cellContext.viewId - ..fieldId = cellContext.fieldInfo.id - ..rowId = cellContext.rowId + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId ..insertOptions.add(name); return DatabaseEventUpdateChecklistCell(payload).send(); @@ -27,9 +32,9 @@ class ChecklistCellBackendService { required List optionIds, }) { final payload = ChecklistCellDataChangesetPB.create() - ..viewId = cellContext.viewId - ..fieldId = cellContext.fieldInfo.id - ..rowId = cellContext.rowId + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId ..deleteOptionIds.addAll(optionIds); return DatabaseEventUpdateChecklistCell(payload).send(); @@ -39,9 +44,9 @@ class ChecklistCellBackendService { required String optionId, }) { final payload = ChecklistCellDataChangesetPB.create() - ..viewId = cellContext.viewId - ..fieldId = cellContext.fieldInfo.id - ..rowId = cellContext.rowId + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId ..selectedOptionIds.add(optionId); return DatabaseEventUpdateChecklistCell(payload).send(); @@ -51,9 +56,9 @@ class ChecklistCellBackendService { required SelectOptionPB option, }) { final payload = ChecklistCellDataChangesetPB.create() - ..viewId = cellContext.viewId - ..fieldId = cellContext.fieldInfo.id - ..rowId = cellContext.rowId + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId ..updateOptions.add(option); return DatabaseEventUpdateChecklistCell(payload).send(); @@ -61,10 +66,9 @@ class ChecklistCellBackendService { Future> getCellData() { final payload = CellIdPB.create() - ..fieldId = cellContext.fieldInfo.id - ..viewId = cellContext.viewId - ..rowId = cellContext.rowId - ..rowId = cellContext.rowId; + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; return DatabaseEventGetChecklistCellData(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart index c95bc33251..6121bdc969 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/select_option_cell_service.dart @@ -1,6 +1,4 @@ -import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -8,12 +6,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; class SelectOptionCellBackendService { - final DatabaseCellContext cellContext; - SelectOptionCellBackendService({required this.cellContext}); + final String viewId; + final String fieldId; + final String rowId; - String get viewId => cellContext.viewId; - String get fieldId => cellContext.fieldInfo.id; - RowId get rowId => cellContext.rowId; + SelectOptionCellBackendService({ + required this.viewId, + required this.fieldId, + required this.rowId, + }); Future> create({ required String name, 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 2e261a449e..1a2082593f 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 @@ -160,7 +160,7 @@ class DatabaseController { }); } - Future> createRow({ + Future> createRow({ RowId? startRowId, String? groupId, void Function(RowDataBuilder builder)? withCells, @@ -181,9 +181,9 @@ class DatabaseController { } Future> moveGroupRow({ - required RowPB fromRow, + required RowMetaPB fromRow, required String groupId, - RowPB? toRow, + RowMetaPB? toRow, }) { return _databaseViewBackendSvc.moveGroupRow( fromRowId: fromRow.id, @@ -193,12 +193,12 @@ class DatabaseController { } Future> moveRow({ - required RowPB fromRow, - required RowPB toRow, + required String fromRowId, + required String toRowId, }) { return _databaseViewBackendSvc.moveRow( - fromRowId: fromRow.id, - toRowId: toRow.id, + fromRowId: fromRowId, + toRowId: toRowId, ); } @@ -269,8 +269,8 @@ class DatabaseController { onRowsDeleted: (ids) { _databaseCallbacks?.onRowsDeleted?.call(ids); }, - onRowsUpdated: (ids) { - _databaseCallbacks?.onRowsUpdated?.call(ids); + onRowsUpdated: (ids, reason) { + _databaseCallbacks?.onRowsUpdated?.call(ids, reason); }, onRowsCreated: (ids) { _databaseCallbacks?.onRowsCreated?.call(ids); 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 feee9c7d7b..64f9a2f1b6 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 @@ -30,7 +30,7 @@ class DatabaseViewBackendService { return DatabaseEventGetDatabase(payload).send(); } - Future> createRow({ + Future> createRow({ RowId? startRowId, String? groupId, Map? cellDataByFieldId, 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 64bf27538c..59ae345e2b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -13,7 +13,10 @@ typedef OnFiltersChanged = void Function(List); typedef OnDatabaseChanged = void Function(DatabasePB); typedef OnRowsCreated = void Function(List ids); -typedef OnRowsUpdated = void Function(List ids); +typedef OnRowsUpdated = void Function( + List ids, + RowsChangedReason reason, +); typedef OnRowsDeleted = void Function(List ids); typedef OnNumOfRowsChanged = void Function( UnmodifiableListView rows, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart index 40980a9c51..f5b91c3a8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart @@ -1,4 +1,3 @@ -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -52,14 +51,11 @@ class FieldCellBloc extends Bloc { void _startListening() { _fieldListener.start( - onFieldChanged: (result) { + onFieldChanged: (updatedField) { if (isClosed) { return; } - result.fold( - (field) => add(FieldCellEvent.didReceiveFieldUpdate(field)), - (err) => Log.error(err), - ); + add(FieldCellEvent.didReceiveFieldUpdate(updatedField)); }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart index d3597dc046..09ecaac807 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart @@ -1,4 +1,5 @@ import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; @@ -7,12 +8,11 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -typedef UpdateFieldNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = FieldPB; class SingleFieldListener { final String fieldId; - PublishNotifier? _updateFieldNotifier = - PublishNotifier(); + void Function(UpdateFieldNotifiedValue)? _updateFieldNotifier; DatabaseNotificationListener? _listener; SingleFieldListener({required this.fieldId}); @@ -20,7 +20,7 @@ class SingleFieldListener { void start({ required void Function(UpdateFieldNotifiedValue) onFieldChanged, }) { - _updateFieldNotifier?.addPublishListener(onFieldChanged); + _updateFieldNotifier = onFieldChanged; _listener = DatabaseNotificationListener( objectId: fieldId, handler: _handler, @@ -34,9 +34,8 @@ class SingleFieldListener { switch (ty) { case DatabaseNotification.DidUpdateField: result.fold( - (payload) => - _updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)), - (error) => _updateFieldNotifier?.value = right(error), + (payload) => _updateFieldNotifier?.call(FieldPB.fromBuffer(payload)), + (error) => Log.error(error), ); break; default: @@ -46,7 +45,6 @@ class SingleFieldListener { Future stop() async { await _listener?.stop(); - _updateFieldNotifier?.dispose(); _updateFieldNotifier = null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart index 413c1d3e7a..05b1b30e71 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -99,6 +99,14 @@ class FieldBackendService { ); }); } + + /// Returns the primary field of the view. + static Future> getPrimaryField({ + required String viewId, + }) { + final payload = DatabaseViewIdPB.create()..value = viewId; + return DatabaseEventGetPrimaryField(payload).send(); + } } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart new file mode 100644 index 0000000000..f06b74d77a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_banner_bloc.dart @@ -0,0 +1,163 @@ +import 'package:appflowy/plugins/database_view/application/field/field_listener.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'row_meta_listener.dart'; + +part 'row_banner_bloc.freezed.dart'; + +class RowBannerBloc extends Bloc { + final String viewId; + final RowBackendService _rowBackendSvc; + final RowMetaListener _metaListener; + SingleFieldListener? _fieldListener; + + RowBannerBloc({ + required this.viewId, + required RowMetaPB rowMeta, + }) : _rowBackendSvc = RowBackendService(viewId: viewId), + _metaListener = RowMetaListener(rowMeta.id), + super(RowBannerState.initial(rowMeta)) { + on( + (event, emit) async { + event.when( + initial: () async { + _loadPrimaryField(); + await _listenRowMeteChanged(); + }, + didReceiveRowMeta: (RowMetaPB rowMeta) { + emit( + state.copyWith( + rowMeta: rowMeta, + ), + ); + }, + setCover: (String coverURL) { + _updateMeta(coverURL: coverURL); + }, + setIcon: (String iconURL) { + _updateMeta(iconURL: iconURL); + }, + didReceiveFieldUpdate: (updatedField) { + emit( + state.copyWith( + primaryField: updatedField, + loadingState: const LoadingState.finish(), + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + await _metaListener.stop(); + await _fieldListener?.stop(); + _fieldListener = null; + + return super.close(); + } + + Future _loadPrimaryField() async { + final fieldOrError = + await FieldBackendService.getPrimaryField(viewId: viewId); + fieldOrError.fold( + (primaryField) { + if (!isClosed) { + _fieldListener = SingleFieldListener(fieldId: primaryField.id); + _fieldListener?.start( + onFieldChanged: (updatedField) { + if (!isClosed) { + add(RowBannerEvent.didReceiveFieldUpdate(updatedField)); + } + }, + ); + add(RowBannerEvent.didReceiveFieldUpdate(primaryField)); + } + }, + (r) => Log.error(r), + ); + } + + /// Listen the changes of the row meta and then update the banner + Future _listenRowMeteChanged() async { + _metaListener.start( + callback: (rowMeta) { + add(RowBannerEvent.didReceiveRowMeta(rowMeta)); + }, + ); + } + + /// Update the meta of the row and the view + Future _updateMeta({ + String? iconURL, + String? coverURL, + }) async { + // Most of the time, the result is success, so we don't need to handle it. + await _rowBackendSvc + .updateMeta( + iconURL: iconURL, + coverURL: coverURL, + rowId: state.rowMeta.id, + ) + .then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + + // Set the icon and cover of the view + ViewBackendService.updateView( + viewId: viewId, + iconURL: iconURL, + coverURL: coverURL, + ).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + } +} + +@freezed +class RowBannerEvent with _$RowBannerEvent { + const factory RowBannerEvent.initial() = _Initial; + const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) = + _DidReceiveRowMeta; + const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) = + _DidReceiveFieldUdate; + const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon; + const factory RowBannerEvent.setCover(String coverURL) = _SetCover; +} + +@freezed +class RowBannerState with _$RowBannerState { + const factory RowBannerState({ + ViewPB? view, + FieldPB? primaryField, + required RowMetaPB rowMeta, + required LoadingState loadingState, + }) = _RowBannerState; + + factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState( + rowMeta: rowMetaPB, + loadingState: const LoadingState.loading(), + ); +} + +@freezed +class LoadingState with _$LoadingState { + const factory LoadingState.loading() = _Loading; + const factory LoadingState.finish() = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 9d646394f4..83656a7ff9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -38,17 +38,21 @@ class RowCache { final RowCacheDelegate _delegate; final RowChangesetNotifier _rowChangeReasonNotifier; + /// Returns a unmodifiable list of RowInfo UnmodifiableListView get rowInfos { final visibleRows = [..._rowList.rows]; return UnmodifiableListView(visibleRows); } + /// Returns a unmodifiable map of rowId to RowInfo UnmodifiableMapView get rowByRowId { return UnmodifiableMapView(_rowList.rowInfoByRowId); } CellCache get cellCache => _cellCache; + RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason; + RowCache({ required this.viewId, required RowFieldsDelegate fieldsDelegate, @@ -70,7 +74,7 @@ class RowCache { return _rowList.get(rowId); } - void setInitialRows(List rows) { + void setInitialRows(List rows) { for (final row in rows) { final rowInfo = buildGridRow(row); _rowList.add(rowInfo); @@ -128,7 +132,7 @@ class RowCache { void _insertRows(List insertRows) { for (final insertedRow in insertRows) { final insertedIndex = - _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row)); + _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); if (insertedIndex != null) { _rowChangeReasonNotifier .receive(RowsChangedReason.insert(insertedIndex)); @@ -138,20 +142,23 @@ class RowCache { void _updateRows(List updatedRows) { if (updatedRows.isEmpty) return; - final List rowPBs = []; + final List updatedList = []; for (final updatedRow in updatedRows) { for (final fieldId in updatedRow.fieldIds) { final key = CellCacheKey( fieldId: fieldId, - rowId: updatedRow.row.id, + rowId: updatedRow.rowId, ); _cellCache.remove(key); } - rowPBs.add(updatedRow.row); + if (updatedRow.hasRowMeta()) { + updatedList.add(updatedRow.rowMeta); + } } final updatedIndexs = - _rowList.updateRows(rowPBs, (rowPB) => buildGridRow(rowPB)); + _rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId)); + if (updatedIndexs.isNotEmpty) { _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); } @@ -169,7 +176,7 @@ class RowCache { void _showRows(List visibleRows) { for (final insertedRow in visibleRows) { final insertedIndex = - _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row)); + _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); if (insertedIndex != null) { _rowChangeReasonNotifier .receive(RowsChangedReason.insert(insertedIndex)); @@ -197,8 +204,9 @@ class RowCache { if (onCellUpdated != null) { final rowInfo = _rowList.get(rowId); if (rowInfo != null) { - final CellContextByFieldId cellDataMap = - _makeGridCells(rowId, rowInfo.rowPB); + final CellContextByFieldId cellDataMap = _makeGridCells( + rowInfo.rowMeta, + ); onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); } } @@ -220,12 +228,12 @@ class RowCache { _rowChangeReasonNotifier.removeListener(callback); } - CellContextByFieldId loadGridCells(RowId rowId) { - final RowPB? data = _rowList.get(rowId)?.rowPB; - if (data == null) { - _loadRow(rowId); + CellContextByFieldId loadGridCells(RowMetaPB rowMeta) { + final rowInfo = _rowList.get(rowMeta.id); + if (rowInfo == null) { + _loadRow(rowMeta.id); } - return _makeGridCells(rowId, data); + return _makeGridCells(rowMeta); } Future _loadRow(RowId rowId) async { @@ -233,57 +241,51 @@ class RowCache { ..viewId = viewId ..rowId = rowId; - final result = await DatabaseEventGetRow(payload).send(); + final result = await DatabaseEventGetRowMeta(payload).send(); result.fold( - (optionRow) => _refreshRow(optionRow), + (rowMetaPB) { + final rowInfo = _rowList.get(rowMetaPB.id); + final rowIndex = _rowList.indexOfRow(rowMetaPB.id); + if (rowInfo != null && rowIndex != null) { + final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB); + _rowList.remove(rowMetaPB.id); + _rowList.insert(rowIndex, updatedRowInfo); + + final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); + updatedIndexs[rowMetaPB.id] = UpdatedIndex( + index: rowIndex, + rowId: rowMetaPB.id, + ); + + _rowChangeReasonNotifier + .receive(RowsChangedReason.update(updatedIndexs)); + } + }, (err) => Log.error(err), ); } - CellContextByFieldId _makeGridCells(RowId rowId, RowPB? row) { + CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) { // ignore: prefer_collection_literals - final cellDataMap = CellContextByFieldId(); + final cellContextMap = CellContextByFieldId(); for (final field in _delegate.fields) { if (field.visibility) { - cellDataMap[field.id] = DatabaseCellContext( - rowId: rowId, + cellContextMap[field.id] = DatabaseCellContext( + rowMeta: rowMeta, viewId: viewId, fieldInfo: field, ); } } - return cellDataMap; + return cellContextMap; } - void _refreshRow(OptionalRowPB optionRow) { - if (!optionRow.hasRow()) { - return; - } - final updatedRow = optionRow.row; - updatedRow.freeze(); - - final rowInfo = _rowList.get(updatedRow.id); - final rowIndex = _rowList.indexOfRow(updatedRow.id); - if (rowInfo != null && rowIndex != null) { - final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow); - _rowList.remove(updatedRow.id); - _rowList.insert(rowIndex, updatedRowInfo); - - final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); - updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex( - index: rowIndex, - rowId: updatedRowInfo.rowPB.id, - ); - - _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); - } - } - - RowInfo buildGridRow(RowPB rowPB) { + RowInfo buildGridRow(RowMetaPB rowMetaPB) { return RowInfo( viewId: viewId, fields: _delegate.fields, - rowPB: rowPB, + rowId: rowMetaPB.id, + rowMeta: rowMetaPB, ); } } @@ -310,9 +312,10 @@ class RowChangesetNotifier extends ChangeNotifier { @unfreezed class RowInfo with _$RowInfo { factory RowInfo({ + required String rowId, required String viewId, required UnmodifiableListView fields, - required RowPB rowPB, + required RowMetaPB rowMeta, }) = _RowInfo; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart index 24fd809633..4dacd7310a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart @@ -1,12 +1,12 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter/material.dart'; import '../cell/cell_service.dart'; import 'row_cache.dart'; -import 'row_service.dart'; typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason); class RowController { - final RowId rowId; + final RowMetaPB rowMeta; final String? groupId; final String viewId; final List _onRowChangedListeners = []; @@ -14,24 +14,27 @@ class RowController { get cellCache => _rowCache.cellCache; + get rowId => rowMeta.id; + RowController({ - required this.rowId, + required this.rowMeta, required this.viewId, required RowCache rowCache, this.groupId, }) : _rowCache = rowCache; CellContextByFieldId loadData() { - return _rowCache.loadGridCells(rowId); + return _rowCache.loadGridCells(rowMeta); } void addListener({OnRowChanged? onRowChanged}) { - _onRowChangedListeners.add( - _rowCache.addListener( - rowId: rowId, - onCellUpdated: onRowChanged, - ), + final fn = _rowCache.addListener( + rowId: rowMeta.id, + onCellUpdated: onRowChanged, ); + + // Add the listener to the list so that we can remove it later. + _onRowChangedListeners.add(fn); } void dispose() { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart index 2db9292ec7..4fd489dd98 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart @@ -25,10 +25,9 @@ class RowList { } void add(RowInfo rowInfo) { - final rowId = rowInfo.rowPB.id; + final rowId = rowInfo.rowId; if (contains(rowId)) { - final index = - _rowInfos.indexWhere((element) => element.rowPB.id == rowId); + final index = _rowInfos.indexWhere((element) => element.rowId == rowId); _rowInfos.removeAt(index); _rowInfos.insert(index, rowInfo); } else { @@ -38,7 +37,7 @@ class RowList { } InsertedIndex? insert(int index, RowInfo rowInfo) { - final rowId = rowInfo.rowPB.id; + final rowId = rowInfo.rowId; var insertedIndex = index; if (_rowInfos.length <= insertedIndex) { insertedIndex = _rowInfos.length; @@ -62,7 +61,7 @@ class RowList { if (rowInfo != null) { final index = _rowInfos.indexOf(rowInfo); if (index != -1) { - rowInfoByRowId.remove(rowInfo.rowPB.id); + rowInfoByRowId.remove(rowInfo.rowId); _rowInfos.remove(rowInfo); } return DeletedIndex(index: index, rowInfo: rowInfo); @@ -73,23 +72,23 @@ class RowList { InsertedIndexs insertRows( List insertedRows, - RowInfo Function(RowPB) builder, + RowInfo Function(RowMetaPB) builder, ) { final InsertedIndexs insertIndexs = []; for (final insertRow in insertedRows) { - final isContains = contains(insertRow.row.id); + final isContains = contains(insertRow.rowMeta.id); var index = insertRow.index; if (_rowInfos.length < index) { index = _rowInfos.length; } - insert(index, builder(insertRow.row)); + insert(index, builder(insertRow.rowMeta)); if (!isContains) { insertIndexs.add( InsertedIndex( index: index, - rowId: insertRow.row.id, + rowId: insertRow.rowMeta.id, ), ); } @@ -105,10 +104,10 @@ class RowList { }; _rowInfos.asMap().forEach((index, RowInfo rowInfo) { - if (deletedRowByRowId[rowInfo.rowPB.id] == null) { + if (deletedRowByRowId[rowInfo.rowId] == null) { newRows.add(rowInfo); } else { - rowInfoByRowId.remove(rowInfo.rowPB.id); + rowInfoByRowId.remove(rowInfo.rowId); deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo)); } }); @@ -117,19 +116,21 @@ class RowList { } UpdatedIndexMap updateRows( - List updatedRows, - RowInfo Function(RowPB) builder, + List rowMetas, + RowInfo Function(RowMetaPB) builder, ) { final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); - for (final RowPB updatedRow in updatedRows) { - final rowId = updatedRow.id; + for (final rowMeta in rowMetas) { final index = _rowInfos.indexWhere( - (rowInfo) => rowInfo.rowPB.id == rowId, + (rowInfo) => rowInfo.rowId == rowMeta.id, ); if (index != -1) { - final rowInfo = builder(updatedRow); + final rowInfo = builder(rowMeta); insert(index, rowInfo); - updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); + updatedIndexs[rowMeta.id] = UpdatedIndex( + index: index, + rowId: rowMeta.id, + ); } } return updatedIndexs; @@ -148,7 +149,7 @@ class RowList { void moveRow(RowId rowId, int oldIndex, int newIndex) { final index = _rowInfos.indexWhere( - (rowInfo) => rowInfo.rowPB.id == rowId, + (rowInfo) => rowInfo.rowId == rowId, ); if (index != -1) { final rowInfo = remove(rowId)!.rowInfo; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart new file mode 100644 index 0000000000..d696240e84 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_meta_listener.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; + +typedef RowMetaCallback = void Function(RowMetaPB); + +class RowMetaListener { + final String rowId; + RowMetaCallback? _callback; + DatabaseNotificationListener? _listener; + RowMetaListener(this.rowId); + + void start({required RowMetaCallback callback}) { + _callback = callback; + _listener = DatabaseNotificationListener( + objectId: rowId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateRowMeta: + result.fold( + (payload) { + if (_callback != null) { + _callback!(RowMetaPB.fromBuffer(payload)); + } + }, + (error) => Log.error(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _callback = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart index 5eecf1ac2b..8668341f78 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart @@ -12,7 +12,7 @@ class RowBackendService { required this.viewId, }); - Future> createRow(RowId rowId) { + Future> createRowAfterRow(RowId rowId) { final payload = CreateRowPayloadPB.create() ..viewId = viewId ..startRowId = rowId; @@ -28,6 +28,33 @@ class RowBackendService { return DatabaseEventGetRow(payload).send(); } + Future> getRowMeta(RowId rowId) { + final payload = RowIdPB.create() + ..viewId = viewId + ..rowId = rowId; + + return DatabaseEventGetRowMeta(payload).send(); + } + + Future> updateMeta({ + required String rowId, + String? iconURL, + String? coverURL, + }) { + final payload = UpdateRowMetaChangesetPB.create() + ..viewId = viewId + ..id = rowId; + + if (iconURL != null) { + payload.iconUrl = iconURL; + } + if (coverURL != null) { + payload.coverUrl = coverURL; + } + + return DatabaseEventUpdateRowMeta(payload).send(); + } + Future> deleteRow(RowId rowId) { final payload = RowIdPB.create() ..viewId = viewId 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 70eb76a5d9..c44caaf1af 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 @@ -65,14 +65,16 @@ class DatabaseViewCache { } if (changeset.updatedRows.isNotEmpty) { - _callbacks?.onRowsUpdated - ?.call(changeset.updatedRows.map((e) => e.row.id).toList()); + _callbacks?.onRowsUpdated?.call( + changeset.updatedRows.map((e) => e.rowId).toList(), + _rowCache.changeReason, + ); } if (changeset.insertedRows.isNotEmpty) { _callbacks?.onRowsCreated?.call( changeset.insertedRows - .map((insertedRow) => insertedRow.row.id) + .map((insertedRow) => insertedRow.rowMeta.id) .toList(), ); } 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 9bdddec1f6..eafbfc9e3d 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 @@ -156,7 +156,7 @@ class BoardBloc extends Bloc { ); } - void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) { + void _groupItemStartEditing(GroupPB group, RowMetaPB row, bool isEdit) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { Log.warn("fieldInfo should not be null"); @@ -302,12 +302,12 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow; const factory BoardEvent.didCreateRow( GroupPB group, - RowPB row, + RowMetaPB row, int? index, ) = _DidCreateRow; const factory BoardEvent.startEditingRow( GroupPB group, - RowPB row, + RowMetaPB row, ) = _StartEditRow; const factory BoardEvent.endEditingRow(RowId rowId) = _EndEditRow; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; @@ -371,7 +371,7 @@ class GridFieldEquatable extends Equatable { } class GroupItem extends AppFlowyGroupItem { - final RowPB row; + final RowMetaPB row; final FieldInfo fieldInfo; GroupItem({ @@ -389,7 +389,7 @@ class GroupItem extends AppFlowyGroupItem { class GroupControllerDelegateImpl extends GroupControllerDelegate { final FieldController fieldController; final AppFlowyBoardController controller; - final void Function(String, RowPB, int?) onNewColumnItem; + final void Function(String, RowMetaPB, int?) onNewColumnItem; GroupControllerDelegateImpl({ required this.controller, @@ -398,7 +398,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { }); @override - void insertRow(GroupPB group, RowPB row, int? index) { + void insertRow(GroupPB group, RowMetaPB row, int? index) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { Log.warn("fieldInfo should not be null"); @@ -426,7 +426,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } @override - void updateRow(GroupPB group, RowPB row) { + void updateRow(GroupPB group, RowMetaPB row) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { Log.warn("fieldInfo should not be null"); @@ -442,7 +442,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } @override - void addNewRow(GroupPB group, RowPB row, int? index) { + void addNewRow(GroupPB group, RowMetaPB row, int? index) { final fieldInfo = fieldController.getField(group.fieldId); if (fieldInfo == null) { Log.warn("fieldInfo should not be null"); @@ -465,7 +465,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { class BoardEditingRow { GroupPB group; - RowPB row; + RowMetaPB row; int? index; BoardEditingRow({ diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart index df7e91d19a..0e4a315de3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart @@ -12,9 +12,9 @@ typedef OnGroupError = void Function(FlowyError); abstract class GroupControllerDelegate { void removeRow(GroupPB group, RowId rowId); - void insertRow(GroupPB group, RowPB row, int? index); - void updateRow(GroupPB group, RowPB row); - void addNewRow(GroupPB group, RowPB row, int? index); + void insertRow(GroupPB group, RowMetaPB row, int? index); + void updateRow(GroupPB group, RowMetaPB row); + void addNewRow(GroupPB group, RowMetaPB row, int? index); } class GroupController { @@ -28,7 +28,7 @@ class GroupController { required this.delegate, }) : _listener = SingleGroupListener(group); - RowPB? rowAtIndex(int index) { + RowMetaPB? rowAtIndex(int index) { if (index < group.rows.length) { return group.rows[index]; } else { @@ -36,7 +36,7 @@ class GroupController { } } - RowPB? lastRow() { + RowMetaPB? lastRow() { if (group.rows.isEmpty) return null; return group.rows.last; } @@ -55,15 +55,15 @@ class GroupController { final index = insertedRow.hasIndex() ? insertedRow.index : null; if (insertedRow.hasIndex() && group.rows.length > insertedRow.index) { - group.rows.insert(insertedRow.index, insertedRow.row); + group.rows.insert(insertedRow.index, insertedRow.rowMeta); } else { - group.rows.add(insertedRow.row); + group.rows.add(insertedRow.rowMeta); } if (insertedRow.isNew) { - delegate.addNewRow(group, insertedRow.row, index); + delegate.addNewRow(group, insertedRow.rowMeta, index); } else { - delegate.insertRow(group, insertedRow.row, index); + delegate.insertRow(group, insertedRow.rowMeta, index); } } 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 8259b39e20..9763d8431f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/board.dart @@ -45,8 +45,12 @@ class BoardPlugin extends Plugin { BoardPlugin({ required ViewPB view, required PluginType pluginType, + bool listenOnViewChanged = false, }) : _pluginType = pluginType, - notifier = ViewPluginNotifier(view: view); + notifier = ViewPluginNotifier( + view: view, + listenOnViewChanged: listenOnViewChanged, + ); @override PluginWidgetBuilder get widgetBuilder => 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 deb02004d2..c74d4e791c 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 @@ -231,7 +231,7 @@ class _BoardContentState extends State { ) { final groupItem = afGroupItem as GroupItem; final groupData = afGroupData.customData as GroupData; - final rowPB = groupItem.row; + final rowMeta = groupItem.row; final rowCache = context.read().getRowCache(); /// Return placeholder widget if the rowCache is null. @@ -255,7 +255,7 @@ class _BoardContentState extends State { margin: config.cardPadding, decoration: _makeBoxDecoration(context), child: RowCard( - row: rowPB, + rowMeta: rowMeta, viewId: viewId, rowCache: rowCache, cardData: groupData.group.groupId, @@ -267,7 +267,7 @@ class _BoardContentState extends State { viewId, groupData.group.groupId, fieldController, - rowPB, + rowMeta, rowCache, context, ), @@ -305,18 +305,19 @@ class _BoardContentState extends State { String viewId, String groupId, FieldController fieldController, - RowPB rowPB, + RowMetaPB rowMetaPB, RowCache rowCache, BuildContext context, ) { final rowInfo = RowInfo( viewId: viewId, fields: UnmodifiableListView(fieldController.fieldInfos), - rowPB: rowPB, + rowMeta: rowMetaPB, + rowId: rowMetaPB.id, ); final dataController = RowController( - rowId: rowInfo.rowPB.id, + rowMeta: rowInfo.rowMeta, viewId: rowInfo.viewId, rowCache: rowCache, groupId: groupId, 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 825221afef..63c007e55c 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 @@ -268,7 +268,7 @@ class CalendarBloc extends Bloc { final eventData = CalendarDayEvent( event: eventPB, - eventId: eventPB.rowId, + eventId: eventPB.rowMeta.id, dateFieldId: eventPB.dateFieldId, date: date, ); @@ -310,7 +310,7 @@ class CalendarBloc extends Bloc { } add(CalendarEvent.didDeleteEvents(rowIds)); }, - onRowsUpdated: (rowIds) async { + onRowsUpdated: (rowIds, reason) async { if (isClosed) { return; } 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 7990932a1f..c9a4249ea7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart @@ -45,8 +45,12 @@ class CalendarPlugin extends Plugin { CalendarPlugin({ required ViewPB view, required PluginType pluginType, + bool listenOnViewChanged = false, }) : _pluginType = pluginType, - notifier = ViewPluginNotifier(view: view); + notifier = ViewPluginNotifier( + view: view, + listenOnViewChanged: listenOnViewChanged, + ); @override PluginWidgetBuilder get widgetBuilder => 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 a9cffd8379..241e276c57 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 @@ -301,7 +301,7 @@ class _EventCard extends StatelessWidget { // Add the key here to make sure the card is rebuilt when the cells // in this row are updated. key: ValueKey(event.eventId), - row: rowInfo!.rowPB, + rowMeta: rowInfo!.rowMeta, viewId: viewId, rowCache: rowCache, cardData: event.dateFieldId, 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 388c82df66..5efc020bd2 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 @@ -243,7 +243,7 @@ void showEventDetails({ required RowCache rowCache, }) { final dataController = RowController( - rowId: event.eventId, + rowMeta: event.event.rowMeta, viewId: viewId, rowCache: rowCache, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/database_view.dart b/frontend/appflowy_flutter/lib/plugins/database_view/database_view.dart deleted file mode 100644 index 4b9ddfe4bf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/database_view.dart +++ /dev/null @@ -1,61 +0,0 @@ -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:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import '../../workspace/presentation/home/home_stack.dart'; - -/// [DatabaseViewPlugin] is used to build the grid, calendar, and board. -/// It is a wrapper of the [Plugin] class. The underlying [Plugin] is -/// determined by the [ViewPB.pluginType] field. -/// -class DatabaseViewPlugin extends Plugin { - final ViewListener _viewListener; - ViewPB _view; - Plugin _innerPlugin; - - DatabaseViewPlugin({ - required ViewPB view, - }) : _view = view, - _innerPlugin = _makeInnerPlugin(view), - _viewListener = ViewListener(view: view) { - _listenOnLayoutChanged(); - } - - @override - PluginId get id => _innerPlugin.id; - - @override - PluginType get pluginType => _innerPlugin.pluginType; - - @override - PluginWidgetBuilder get widgetBuilder => _innerPlugin.widgetBuilder; - - @override - void dispose() { - _viewListener.stop(); - super.dispose(); - } - - void _listenOnLayoutChanged() { - _viewListener.start( - onViewUpdated: (result) { - result.fold( - (updatedView) { - if (_view.layout != updatedView.layout) { - _innerPlugin = _makeInnerPlugin(updatedView); - - getIt().setPlugin(_innerPlugin); - } - _view = updatedView; - }, - (r) => null, - ); - }, - ); - } -} - -Plugin _makeInnerPlugin(ViewPB view) { - return makePlugin(pluginType: view.pluginType, data: view); -} 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 f688b3a6e7..92f3da700a 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 @@ -33,18 +33,18 @@ class GridBloc extends Bloc { final rowService = RowBackendService( viewId: rowInfo.viewId, ); - await rowService.deleteRow(rowInfo.rowPB.id); + await rowService.deleteRow(rowInfo.rowId); }, moveRow: (int from, int to) { final List rows = [...state.rowInfos]; - final fromRow = rows[from].rowPB; - final toRow = rows[to].rowPB; + final fromRow = rows[from].rowId; + final toRow = rows[to].rowId; rows.insert(to, rows.removeAt(from)); emit(state.copyWith(rowInfos: rows)); - databaseController.moveRow(fromRow: fromRow, toRow: toRow); + databaseController.moveRow(fromRowId: fromRow, toRowId: toRow); }, didReceiveGridUpdate: (grid) { emit(state.copyWith(grid: Some(grid))); @@ -56,7 +56,7 @@ class GridBloc extends Bloc { ), ); }, - didReceiveRowUpdate: (newRowInfos, reason) { + didLoadRows: (newRowInfos, reason) { emit( state.copyWith( rowInfos: newRowInfos, @@ -76,7 +76,7 @@ class GridBloc extends Bloc { return super.close(); } - RowCache? getRowCache(RowId rowId) { + RowCache getRowCache(RowId rowId) { return databaseController.rowCache; } @@ -89,9 +89,14 @@ class GridBloc extends Bloc { }, onNumOfRowsChanged: (rowInfos, _, reason) { if (!isClosed) { - add(GridEvent.didReceiveRowUpdate(rowInfos, reason)); + add(GridEvent.didLoadRows(rowInfos, reason)); } }, + onRowsUpdated: (rows, reason) { + add( + GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason), + ); + }, onFieldsChanged: (fields) { if (!isClosed) { add(GridEvent.didReceiveFieldUpdate(fields)); @@ -122,9 +127,9 @@ class GridEvent with _$GridEvent { const factory GridEvent.createRow() = _CreateRow; const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; const factory GridEvent.moveRow(int from, int to) = _MoveRow; - const factory GridEvent.didReceiveRowUpdate( + const factory GridEvent.didLoadRows( List rows, - RowsChangedReason listState, + RowsChangedReason reason, ) = _DidReceiveRowUpdate; const factory GridEvent.didReceiveFieldUpdate( List fields, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart index 1cb067a787..a741f0dd09 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart @@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:dartz/dartz.dart'; -import '../../../application/row/row_cache.dart'; import '../../../application/row/row_service.dart'; part 'row_action_sheet_bloc.freezed.dart'; @@ -13,19 +12,20 @@ class RowActionSheetBloc extends Bloc { final RowBackendService _rowService; - RowActionSheetBloc({required RowInfo rowInfo}) - : _rowService = RowBackendService(viewId: rowInfo.viewId), - super(RowActionSheetState.initial(rowInfo)) { + RowActionSheetBloc({ + required String viewId, + required RowId rowId, + }) : _rowService = RowBackendService(viewId: viewId), + super(RowActionSheetState.initial(rowId)) { on( (event, emit) async { await event.when( deleteRow: () async { - final result = await _rowService.deleteRow(state.rowData.rowPB.id); + final result = await _rowService.deleteRow(state.rowId); logResult(result); }, duplicateRow: () async { - final result = - await _rowService.duplicateRow(rowId: state.rowData.rowPB.id); + final result = await _rowService.duplicateRow(rowId: state.rowId); logResult(result); }, ); @@ -47,10 +47,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent { @freezed class RowActionSheetState with _$RowActionSheetState { const factory RowActionSheetState({ - required RowInfo rowData, + required RowId rowId, }) = _RowActionSheetState; - factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState( - rowData: rowData, + factory RowActionSheetState.initial(RowId rowId) => RowActionSheetState( + rowId: rowId, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart index bdc25fac78..0a108abbdf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart @@ -15,13 +15,16 @@ part 'row_bloc.freezed.dart'; class RowBloc extends Bloc { final RowBackendService _rowBackendSvc; final RowController _dataController; + final String viewId; + final String rowId; RowBloc({ - required RowInfo rowInfo, + required this.rowId, + required this.viewId, required RowController dataController, - }) : _rowBackendSvc = RowBackendService(viewId: rowInfo.viewId), + }) : _rowBackendSvc = RowBackendService(viewId: viewId), _dataController = dataController, - super(RowState.initial(rowInfo, dataController.loadData())) { + super(RowState.initial(dataController.loadData())) { on( (event, emit) async { await event.when( @@ -29,7 +32,7 @@ class RowBloc extends Bloc { await _startListening(); }, createRow: () { - _rowBackendSvc.createRow(rowInfo.rowPB.id); + _rowBackendSvc.createRowAfterRow(rowId); }, didReceiveCells: (cellByFieldId, reason) async { final cells = cellByFieldId.values @@ -78,18 +81,15 @@ class RowEvent with _$RowEvent { @freezed class RowState with _$RowState { const factory RowState({ - required RowInfo rowInfo, required CellContextByFieldId cellByFieldId, required UnmodifiableListView cells, RowsChangedReason? changeReason, }) = _RowState; factory RowState.initial( - RowInfo rowInfo, CellContextByFieldId cellByFieldId, ) => RowState( - rowInfo: rowInfo, cellByFieldId: cellByFieldId, cells: UnmodifiableListView( cellByFieldId.values diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart index 15d2c7644b..812a98b837 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart @@ -27,7 +27,7 @@ class RowDetailBloc extends Bloc { } }, didReceiveCellDatas: (cells) { - emit(state.copyWith(gridCells: cells)); + emit(state.copyWith(cells: cells)); }, deleteField: (fieldId) { _fieldBackendService(fieldId).deleteField(); @@ -95,10 +95,10 @@ class RowDetailEvent with _$RowDetailEvent { @freezed class RowDetailState with _$RowDetailState { const factory RowDetailState({ - required List gridCells, + required List cells, }) = _RowDetailState; factory RowDetailState.initial() => RowDetailState( - gridCells: List.empty(), + cells: List.empty(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart new file mode 100644 index 0000000000..2d6aced74e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart @@ -0,0 +1,126 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; + +import '../../../application/row/row_service.dart'; + +part 'row_document_bloc.freezed.dart'; + +class RowDocumentBloc extends Bloc { + final String rowId; + final RowBackendService _rowBackendSvc; + + RowDocumentBloc({ + required this.rowId, + required String viewId, + }) : _rowBackendSvc = RowBackendService(viewId: viewId), + super(RowDocumentState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + _getRowDocumentView(); + }, + didReceiveRowDocument: (view) { + emit( + state.copyWith( + viewPB: view, + loadingState: const LoadingState.finish(), + ), + ); + }, + didReceiveError: (FlowyError error) { + emit( + state.copyWith( + loadingState: LoadingState.error(error), + ), + ); + }, + ); + }, + ); + } + + Future _getRowDocumentView() async { + final rowDetailOrError = await _rowBackendSvc.getRowMeta(rowId); + rowDetailOrError.fold( + (RowMetaPB rowMeta) async { + final viewsOrError = + await ViewBackendService.getView(rowMeta.documentId); + + if (isClosed) { + return; + } + + viewsOrError.fold( + (view) => add(RowDocumentEvent.didReceiveRowDocument(view)), + (error) async { + if (error.code == ErrorCode.RecordNotFound.value) { + // By default, the document of the row is not exist. So creating a + // new document for the given document id of the row. + final documentView = + await _createRowDocumentView(rowMeta.documentId); + if (documentView != null) { + add(RowDocumentEvent.didReceiveRowDocument(documentView)); + } + } else { + add(RowDocumentEvent.didReceiveError(error)); + } + }, + ); + }, + (err) => Log.error('Failed to get row detail: $err'), + ); + } + + Future _createRowDocumentView(String viewId) async { + final result = await ViewBackendService.createOrphanView( + viewId: viewId, + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + desc: '', + layoutType: ViewLayoutPB.Document, + ); + return result.fold( + (view) => view, + (error) { + Log.error(error); + return null; + }, + ); + } +} + +@freezed +class RowDocumentEvent with _$RowDocumentEvent { + const factory RowDocumentEvent.initial() = _InitialRow; + const factory RowDocumentEvent.didReceiveRowDocument(ViewPB view) = + _DidReceiveRowDocument; + const factory RowDocumentEvent.didReceiveError(FlowyError error) = + _DidReceiveError; +} + +@freezed +class RowDocumentState with _$RowDocumentState { + const factory RowDocumentState({ + ViewPB? viewPB, + required LoadingState loadingState, + }) = _RowDocumentState; + + factory RowDocumentState.initial() => const RowDocumentState( + loadingState: LoadingState.loading(), + ); +} + +@freezed +class LoadingState with _$LoadingState { + const factory LoadingState.loading() = _Loading; + const factory LoadingState.error(FlowyError error) = _Error; + const factory LoadingState.finish() = _Finish; +} 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 579051cbee..bb44b3aedf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/grid.dart @@ -45,8 +45,12 @@ class GridPlugin extends Plugin { GridPlugin({ required ViewPB view, required PluginType pluginType, + bool listenOnViewChanged = false, }) : _pluginType = pluginType, - notifier = ViewPluginNotifier(view: view); + notifier = ViewPluginNotifier( + view: view, + listenOnViewChanged: listenOnViewChanged, + ); @override PluginWidgetBuilder get widgetBuilder => 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 51344e6a5c..4f5e75ff4b 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/widgets/row/cell_builder.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; @@ -78,7 +80,11 @@ class _GridPageState extends State { loading: (_) => const Center(child: CircularProgressIndicator.adaptive()), finish: (result) => result.successOrFail.fold( - (_) => const GridShortcuts(child: FlowyGrid()), + (_) => GridShortcuts( + child: FlowyGrid( + viewId: widget.view.id, + ), + ), (err) => FlowyErrorPage(err.toString()), ), ); @@ -89,7 +95,9 @@ class _GridPageState extends State { } class FlowyGrid extends StatefulWidget { + final String viewId; const FlowyGrid({ + required this.viewId, super.key, }); @@ -125,6 +133,7 @@ class _FlowyGridState extends State { scrollController: _scrollController, contentWidth: contentWidth, child: _GridRows( + viewId: widget.viewId, verticalScrollController: _scrollController.verticalController, ), ); @@ -155,7 +164,9 @@ class _FlowyGridState extends State { } class _GridRows extends StatelessWidget { + final String viewId; const _GridRows({ + required this.viewId, required this.verticalScrollController, }); @@ -207,7 +218,7 @@ class _GridRows extends StatelessWidget { final rowInfo = rowInfos[index]; return _renderRow( context, - rowInfo, + rowInfo.rowId, index: index, isSortEnabled: sortState.sortInfos.isNotEmpty, isFilterEnabled: filterState.filters.isNotEmpty, @@ -223,38 +234,38 @@ class _GridRows extends StatelessWidget { Widget _renderRow( BuildContext context, - RowInfo rowInfo, { + RowId rowId, { int? index, bool isSortEnabled = false, bool isFilterEnabled = false, Animation? animation, }) { - final rowCache = context.read().getRowCache( - rowInfo.rowPB.id, - ); + final rowCache = context.read().getRowCache(rowId); + final rowMeta = rowCache.getRow(rowId)?.rowMeta; - /// Return placeholder widget if the rowCache is null. - if (rowCache == null) return const SizedBox.shrink(); + /// Return placeholder widget if the rowMeta is null. + if (rowMeta == null) return const SizedBox.shrink(); final fieldController = context.read().databaseController.fieldController; final dataController = RowController( - rowId: rowInfo.rowPB.id, - viewId: rowInfo.viewId, + viewId: viewId, + rowMeta: rowMeta, rowCache: rowCache, ); final child = GridRow( - key: ValueKey(rowInfo.rowPB.id), + key: ValueKey(rowMeta.id), + rowId: rowId, + viewId: viewId, index: index, isDraggable: !isSortEnabled && !isFilterEnabled, - rowInfo: rowInfo, dataController: dataController, cellBuilder: GridCellBuilder(cellCache: dataController.cellCache), openDetailPage: (context, cellBuilder) { _openRowDetailPage( context, - rowInfo, + rowId, fieldController, rowCache, cellBuilder, @@ -274,26 +285,32 @@ class _GridRows extends StatelessWidget { void _openRowDetailPage( BuildContext context, - RowInfo rowInfo, + RowId rowId, FieldController fieldController, RowCache rowCache, GridCellBuilder cellBuilder, ) { - final dataController = RowController( - viewId: rowInfo.viewId, - rowId: rowInfo.rowPB.id, - rowCache: rowCache, - ); + final rowMeta = rowCache.getRow(rowId)?.rowMeta; + // Most of the cases, the rowMeta should not be null. + if (rowMeta != null) { + final dataController = RowController( + viewId: viewId, + rowMeta: rowMeta, + rowCache: rowCache, + ); - FlowyOverlay.show( - context: context, - builder: (BuildContext context) { - return RowDetailPage( - cellBuilder: cellBuilder, - rowController: dataController, - ); - }, - ); + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: cellBuilder, + rowController: dataController, + ); + }, + ); + } else { + Log.warn('RowMeta is null for rowId: $rowId'); + } } } @@ -357,10 +374,9 @@ class _RowCountBadge extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ FlowyText.medium( - '${LocaleKeys.grid_row_count.tr()} : ', + rowCountString(rowCount), color: Theme.of(context).hintColor, ), - FlowyText.medium(rowCount.toString()), ], ), ); @@ -368,3 +384,7 @@ class _RowCountBadge extends StatelessWidget { ); } } + +String rowCountString(int count) { + return '${LocaleKeys.grid_row_count.tr()} : $count'; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart index 60c37ef975..09bbf0de99 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_editor.dart @@ -52,11 +52,11 @@ class _FieldEditorState extends State { @override Widget build(BuildContext context) { final List children = [ - _FieldNameTextField(popoverMutex: popoverMutex), + FieldNameTextField(popoverMutex: popoverMutex), if (widget.onDeleted != null) _addDeleteFieldButton(), if (widget.onHidden != null) _addHideFieldButton(), if (!widget.typeOptionLoader.field.isPrimary) - _FieldTypeOptionCell(popoverMutex: popoverMutex), + FieldTypeOptionCell(popoverMutex: popoverMutex), ]; return BlocProvider( create: (context) { @@ -116,10 +116,10 @@ class _FieldEditorState extends State { } } -class _FieldTypeOptionCell extends StatelessWidget { +class FieldTypeOptionCell extends StatelessWidget { final PopoverMutex popoverMutex; - const _FieldTypeOptionCell({ + const FieldTypeOptionCell({ Key? key, required this.popoverMutex, }) : super(key: key); @@ -130,7 +130,7 @@ class _FieldTypeOptionCell extends StatelessWidget { buildWhen: (p, c) => p.field != c.field, builder: (context, state) { return state.field.fold( - () => const SizedBox(), + () => const SizedBox.shrink(), (fieldInfo) { final dataController = context.read().dataController; @@ -145,18 +145,18 @@ class _FieldTypeOptionCell extends StatelessWidget { } } -class _FieldNameTextField extends StatefulWidget { +class FieldNameTextField extends StatefulWidget { final PopoverMutex popoverMutex; - const _FieldNameTextField({ + const FieldNameTextField({ required this.popoverMutex, Key? key, }) : super(key: key); @override - State<_FieldNameTextField> createState() => _FieldNameTextFieldState(); + State createState() => _FieldNameTextFieldState(); } -class _FieldNameTextFieldState extends State<_FieldNameTextField> { +class _FieldNameTextFieldState extends State { final textController = TextEditingController(); FocusNode focusNode = FocusNode(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart index 32594dd2e3..d7bb2cf1f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart @@ -48,7 +48,7 @@ class FieldTypeOptionEditor extends StatelessWidget { ); final List children = [ - _SwitchFieldButton(popoverMutex: popoverMutex), + SwitchFieldButton(popoverMutex: popoverMutex), if (typeOptionWidget != null) typeOptionWidget ]; @@ -73,9 +73,9 @@ class FieldTypeOptionEditor extends StatelessWidget { } } -class _SwitchFieldButton extends StatelessWidget { +class SwitchFieldButton extends StatelessWidget { final PopoverMutex popoverMutex; - const _SwitchFieldButton({ + const SwitchFieldButton({ required this.popoverMutex, Key? key, }) : super(key: key); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart index 33e1a69c7a..6c1f514371 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/action.dart @@ -1,4 +1,4 @@ -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/application/row/row_action_sheet_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -14,13 +14,18 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../layout/sizes.dart'; class RowActions extends StatelessWidget { - final RowInfo rowData; - const RowActions({required this.rowData, Key? key}) : super(key: key); + final String viewId; + final RowId rowId; + const RowActions({ + required this.viewId, + required this.rowId, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => RowActionSheetBloc(rowInfo: rowData), + create: (context) => RowActionSheetBloc(viewId: viewId, rowId: rowId), child: BlocBuilder( builder: (context, state) { final cells = _RowAction.values diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart index 922d1bcc0f..5c54665e46 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_service.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/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -20,7 +20,8 @@ import "package:appflowy/generated/locale_keys.g.dart"; import 'package:easy_localization/easy_localization.dart'; class GridRow extends StatefulWidget { - final RowInfo rowInfo; + final RowId viewId; + final RowId rowId; final RowController dataController; final GridCellBuilder cellBuilder; final void Function(BuildContext, GridCellBuilder) openDetailPage; @@ -30,7 +31,8 @@ class GridRow extends StatefulWidget { const GridRow({ super.key, - required this.rowInfo, + required this.viewId, + required this.rowId, required this.dataController, required this.cellBuilder, required this.openDetailPage, @@ -49,8 +51,9 @@ class _GridRowState extends State { void initState() { super.initState(); _rowBloc = RowBloc( - rowInfo: widget.rowInfo, + rowId: widget.rowId, dataController: widget.dataController, + viewId: widget.viewId, ); _rowBloc.add(const RowEvent.initial()); } @@ -61,7 +64,8 @@ class _GridRowState extends State { value: _rowBloc, child: _RowEnterRegion( child: BlocBuilder( - buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height, + // The row need to rebuild when the cell count changes. + buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length, builder: (context, state) { final content = Expanded( child: RowContent( @@ -126,7 +130,11 @@ class _RowLeadingState extends State<_RowLeading> { direction: PopoverDirection.rightWithCenterAligned, margin: const EdgeInsets.all(6), popupBuilder: (BuildContext popoverContext) { - return RowActions(rowData: context.read().state.rowInfo); + final bloc = context.read(); + return RowActions( + viewId: bloc.viewId, + rowId: bloc.rowId, + ); }, child: Consumer( builder: (context, state, _) { @@ -143,11 +151,11 @@ class _RowLeadingState extends State<_RowLeading> { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const _InsertButton(), + const InsertRowButton(), if (isDraggable) ...[ ReorderableDragStartListener( index: widget.index!, - child: _MenuButton( + child: RowMenuButton( isDragEnabled: isDraggable, openMenu: () { popoverController.show(); @@ -155,7 +163,7 @@ class _RowLeadingState extends State<_RowLeading> { ), ), ] else ...[ - _MenuButton( + RowMenuButton( openMenu: () { popoverController.show(); }, @@ -168,8 +176,8 @@ class _RowLeadingState extends State<_RowLeading> { bool get isDraggable => widget.index != null && widget.isDraggable; } -class _InsertButton extends StatelessWidget { - const _InsertButton({Key? key}) : super(key: key); +class InsertRowButton extends StatelessWidget { + const InsertRowButton({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -188,20 +196,21 @@ class _InsertButton extends StatelessWidget { } } -class _MenuButton extends StatefulWidget { +class RowMenuButton extends StatefulWidget { final VoidCallback openMenu; final bool isDragEnabled; - const _MenuButton({ + const RowMenuButton({ required this.openMenu, this.isDragEnabled = false, + super.key, }); @override - State<_MenuButton> createState() => _MenuButtonState(); + State createState() => _RowMenuButtonState(); } -class _MenuButtonState extends State<_MenuButton> { +class _RowMenuButtonState extends State { @override Widget build(BuildContext context) { return FlowyIconButton( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart index 17af488de4..db825e4fc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -17,7 +17,7 @@ import 'container/card_container.dart'; /// Edit a database row with card style widget class RowCard extends StatefulWidget { - final RowPB row; + final RowMetaPB rowMeta; final String viewId; final String? groupingFieldId; @@ -46,7 +46,7 @@ class RowCard extends StatefulWidget { final RowCardStyleConfiguration styleConfiguration; const RowCard({ - required this.row, + required this.rowMeta, required this.viewId, this.groupingFieldId, required this.isEditing, @@ -81,7 +81,7 @@ class _RowCardState extends State> { viewId: widget.viewId, groupFieldId: widget.groupingFieldId, isEditing: widget.isEditing, - row: widget.row, + rowMeta: widget.rowMeta, rowCache: widget.rowCache, )..add(const RowCardEvent.initial()); @@ -178,7 +178,8 @@ class _RowCardState extends State> { throw UnimplementedError(); case AccessoryType.more: return RowActions( - rowData: context.read().rowInfo(), + viewId: context.read().viewId, + rowId: context.read().rowMeta.id, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart index 25a88030b3..f2af6128c2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -12,24 +12,24 @@ import '../../application/row/row_service.dart'; part 'card_bloc.freezed.dart'; class CardBloc extends Bloc { - final RowPB row; + final RowMetaPB rowMeta; final String? groupFieldId; final RowBackendService _rowBackendSvc; final RowCache _rowCache; VoidCallback? _rowCallback; + final String viewId; CardBloc({ - required this.row, + required this.rowMeta, required this.groupFieldId, - required String viewId, + required this.viewId, required RowCache rowCache, required bool isEditing, }) : _rowBackendSvc = RowBackendService(viewId: viewId), _rowCache = rowCache, super( RowCardState.initial( - row, - _makeCells(groupFieldId, rowCache.loadGridCells(row.id)), + _makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)), isEditing, ), ) { @@ -70,13 +70,14 @@ class CardBloc extends Bloc { fields: UnmodifiableListView( state.cells.map((cell) => cell.fieldInfo).toList(), ), - rowPB: state.rowPB, + rowId: rowMeta.id, + rowMeta: rowMeta, ); } Future _startListening() async { _rowCallback = _rowCache.addListener( - rowId: row.id, + rowId: rowMeta.id, onCellUpdated: (cellMap, reason) { if (!isClosed) { final cells = _makeCells(groupFieldId, cellMap); @@ -118,19 +119,16 @@ class RowCardEvent with _$RowCardEvent { @freezed class RowCardState with _$RowCardState { const factory RowCardState({ - required RowPB rowPB, required List cells, required bool isEditing, RowsChangedReason? changeReason, }) = _RowCardState; factory RowCardState.initial( - RowPB rowPB, List cells, bool isEditing, ) => RowCardState( - rowPB: rowPB, cells: cells, isEditing: isEditing, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart index 69321d3c45..86e7390ec3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/database_view_widget.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -47,26 +46,19 @@ class _DatabaseViewWidgetState extends State { return ValueListenableBuilder( valueListenable: _layoutTypeChangeNotifier, builder: (_, __, ___) { - return makePlugin(pluginType: view.pluginType, data: view) - .widgetBuilder - .buildWidget(); + return view.plugin().widgetBuilder.buildWidget(); }, ); } void _listenOnViewUpdated() { - _listener = ViewListener(view: widget.view) + _listener = ViewListener(viewId: widget.view.id) ..start( - onViewUpdated: (result) { - result.fold( - (updatedView) { - if (mounted) { - view = updatedView; - _layoutTypeChangeNotifier.value = view.layout; - } - }, - (r) => null, - ); + onViewUpdated: (updatedView) { + if (mounted) { + view = updatedView; + _layoutTypeChangeNotifier.value = view.layout; + } }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index ad427be38d..e29423fca2 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -14,6 +14,7 @@ import 'cells/select_option_cell/select_option_cell.dart'; import 'cells/text_cell/text_cell.dart'; import 'cells/url_cell/url_cell.dart'; +/// Build the cell widget in Grid style. class GridCellBuilder { final CellCache cellCache; GridCellBuilder({ diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart new file mode 100644 index 0000000000..0a76539284 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart @@ -0,0 +1,7 @@ +export 'checkbox_cell/checkbox_cell.dart'; +export 'checklist_cell/checklist_cell.dart'; +export 'date_cell/date_cell.dart'; +export 'number_cell/number_cell.dart'; +export 'select_option_cell/select_option_cell.dart'; +export 'text_cell/text_cell.dart'; +export 'url_cell/url_cell.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart index 05e136d833..a8c5594545 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell.dart @@ -40,8 +40,8 @@ class _CheckboxCellState extends GridCellState { child: BlocBuilder( builder: (context, state) { final icon = state.isSelected - ? svgWidget('editor/editor_check') - : svgWidget('editor/editor_uncheck'); + ? const CheckboxCellCheck() + : const CheckboxCellUncheck(); return Align( alignment: Alignment.centerLeft, child: Padding( @@ -82,3 +82,21 @@ class _CheckboxCellState extends GridCellState { } } } + +class CheckboxCellCheck extends StatelessWidget { + const CheckboxCellCheck({super.key}); + + @override + Widget build(BuildContext context) { + return svgWidget('editor/editor_check'); + } +} + +class CheckboxCellUncheck extends StatelessWidget { + const CheckboxCellUncheck({super.key}); + + @override + Widget build(BuildContext context) { + return svgWidget('editor/editor_uncheck'); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart index e0913008a9..d8445d7f4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart @@ -16,7 +16,9 @@ class ChecklistCardCellBloc ChecklistCardCellBloc({ required this.cellController, }) : _checklistCellSvc = ChecklistCellBackendService( - cellContext: cellController.cellContext, + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, ), super(ChecklistCellState.initial(cellController)) { on( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart index ed7f5dd10d..f48608b389 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart @@ -19,7 +19,9 @@ class ChecklistCellEditorBloc ChecklistCellEditorBloc({ required this.cellController, }) : _checklistCellService = ChecklistCellBackendService( - cellContext: cellController.cellContext, + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, ), super(ChecklistCellEditorState.initial(cellController)) { on( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart index 52f84155ba..04fe36dcd0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart @@ -22,7 +22,7 @@ class ChecklistProgressBar extends StatelessWidget { percent: percent, padding: EdgeInsets.zero, progressColor: percent < 1.0 - ? SelectOptionColorPB.Blue.toColor(context) + ? SelectOptionColorPB.Purple.toColor(context) : SelectOptionColorPB.Green.toColor(context), backgroundColor: AFThemeExtension.of(context).progressBarBGColor, barRadius: const Radius.circular(5), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart index 667e66d913..71231a31d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart @@ -17,7 +17,9 @@ class SelectOptionCellEditorBloc SelectOptionCellEditorBloc({ required this.cellController, }) : _selectOptionService = SelectOptionCellBackendService( - cellContext: cellController.cellContext, + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, ), super(SelectOptionEditorState.initial(cellController)) { on( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart index 89fc12a568..4fc99b9bfe 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../grid/presentation/layout/sizes.dart'; @@ -10,17 +11,23 @@ class GridTextCellStyle extends GridCellStyle { String? placeholder; TextStyle? textStyle; bool? autofocus; + double emojiFontSize; + double emojiHPadding; + bool showEmoji; GridTextCellStyle({ this.placeholder, this.textStyle, this.autofocus, + this.showEmoji = true, + this.emojiFontSize = 16, + this.emojiHPadding = 0, }); } class GridTextCell extends GridCellWidget { final CellControllerBuilder cellControllerBuilder; - late final GridTextCellStyle? cellStyle; + late final GridTextCellStyle cellStyle; GridTextCell({ required this.cellControllerBuilder, GridCellStyle? style, @@ -29,7 +36,7 @@ class GridTextCell extends GridCellWidget { if (style != null) { cellStyle = (style as GridTextCellStyle); } else { - cellStyle = null; + cellStyle = GridTextCellStyle(); } } @@ -66,22 +73,40 @@ class _GridTextCellState extends GridFocusNodeCellState { left: GridSize.cellContentInsets.left, right: GridSize.cellContentInsets.right, ), - child: TextField( - controller: _controller, - focusNode: focusNode, - maxLines: null, - style: widget.cellStyle?.textStyle ?? - Theme.of(context).textTheme.bodyMedium, - autofocus: widget.cellStyle?.autofocus ?? false, - decoration: InputDecoration( - contentPadding: EdgeInsets.only( - top: GridSize.cellContentInsets.top, - bottom: GridSize.cellContentInsets.bottom, - ), - border: InputBorder.none, - hintText: widget.cellStyle?.placeholder, - isDense: true, - ), + child: Row( + children: [ + if (widget.cellStyle.showEmoji) + // Only build the emoji when it changes + BlocBuilder( + buildWhen: (p, c) => p.emoji != c.emoji, + builder: (context, state) => Center( + child: FlowyText( + state.emoji, + fontSize: widget.cellStyle.emojiFontSize, + ), + ), + ), + HSpace(widget.cellStyle.emojiHPadding), + Expanded( + child: TextField( + controller: _controller, + focusNode: focusNode, + maxLines: null, + style: widget.cellStyle.textStyle ?? + Theme.of(context).textTheme.bodyMedium, + autofocus: widget.cellStyle.autofocus ?? false, + decoration: InputDecoration( + contentPadding: EdgeInsets.only( + top: GridSize.cellContentInsets.top, + bottom: GridSize.cellContentInsets.bottom, + ), + border: InputBorder.none, + hintText: widget.cellStyle.placeholder, + isDense: true, + ), + ), + ) + ], ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart index b5ef708a92..987232a1c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart @@ -26,6 +26,9 @@ class TextCellBloc extends Bloc { didReceiveCellUpdate: (content) { emit(state.copyWith(content: content)); }, + didUpdateEmoji: (String emoji) { + emit(state.copyWith(emoji: emoji)); + }, ); }, ); @@ -48,6 +51,11 @@ class TextCellBloc extends Bloc { add(TextCellEvent.didReceiveCellUpdate(cellContent ?? "")); } }), + onRowMetaChanged: () { + if (!isClosed) { + add(TextCellEvent.didUpdateEmoji(cellController.emoji ?? "")); + } + }, ); } } @@ -58,15 +66,18 @@ class TextCellEvent with _$TextCellEvent { const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate; const factory TextCellEvent.updateText(String text) = _UpdateText; + const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji; } @freezed class TextCellState with _$TextCellState { const factory TextCellState({ required String content, + required String emoji, }) = _TextCellState; factory TextCellState.initial(TextCellController context) => TextCellState( content: context.getCellData() ?? "", + emoji: context.emoji ?? "", ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart new file mode 100644 index 0000000000..e9bc70b5a3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart @@ -0,0 +1,174 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.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:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class RowActionList extends StatelessWidget { + final RowController rowController; + const RowActionList({ + required String viewId, + required this.rowController, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10), + child: FlowyText(LocaleKeys.grid_row_action.tr()), + ), + const VSpace(15), + RowDetailPageDeleteButton(rowId: rowController.rowId), + RowDetailPageDuplicateButton( + rowId: rowController.rowId, + groupId: rowController.groupId, + ), + ], + ); + } +} + +class RowDetailPageDeleteButton extends StatelessWidget { + final String rowId; + const RowDetailPageDeleteButton({required this.rowId, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()), + leftIcon: const FlowySvg(name: "home/trash"), + onTap: () { + context.read().add(RowDetailEvent.deleteRow(rowId)); + FlowyOverlay.pop(context); + }, + ), + ); + } +} + +class RowDetailPageDuplicateButton extends StatelessWidget { + final String rowId; + final String? groupId; + const RowDetailPageDuplicateButton({ + required this.rowId, + this.groupId, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()), + leftIcon: const FlowySvg(name: "grid/duplicate"), + onTap: () { + context + .read() + .add(RowDetailEvent.duplicateRow(rowId, groupId)); + FlowyOverlay.pop(context); + }, + ), + ); + } +} + +class CreateRowFieldButton extends StatefulWidget { + final String viewId; + + const CreateRowFieldButton({ + required this.viewId, + Key? key, + }) : super(key: key); + + @override + State createState() => _CreateRowFieldButtonState(); +} + +class _CreateRowFieldButtonState extends State { + late PopoverController popoverController; + late TypeOptionPB typeOption; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(240, 200)), + controller: popoverController, + direction: PopoverDirection.topWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + child: SizedBox( + height: 40, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_field_newProperty.tr(), + color: AFThemeExtension.of(context).textColor, + ), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: () async { + final result = await TypeOptionBackendService.createFieldTypeOption( + viewId: widget.viewId, + ); + result.fold( + (l) { + typeOption = l; + popoverController.show(); + }, + (r) => Log.error("Failed to create field type option: $r"), + ); + }, + leftIcon: svgWidget( + "home/add", + color: AFThemeExtension.of(context).textColor, + ), + ), + ), + popupBuilder: (BuildContext popOverContext) { + return FieldEditor( + viewId: widget.viewId, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.viewId, + field: typeOption.field_2, + ), + onDeleted: (fieldId) { + popoverController.close(); + NavigatorAlertDialog( + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + confirm: () { + context + .read() + .add(RowDetailEvent.deleteField(fieldId)); + }, + ).show(context); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart new file mode 100644 index 0000000000..972efb0854 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_banner.dart @@ -0,0 +1,268 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/emoji_picker/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +typedef RowBannerCellBuilder = Widget Function(String fieldId); + +class RowBanner extends StatefulWidget { + final String viewId; + final RowMetaPB rowMeta; + final RowBannerCellBuilder cellBuilder; + const RowBanner({ + required this.viewId, + required this.rowMeta, + required this.cellBuilder, + super.key, + }); + + @override + State createState() => _RowBannerState(); +} + +class _RowBannerState extends State { + final _isHovering = ValueNotifier(false); + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RowBannerBloc( + viewId: widget.viewId, + rowMeta: widget.rowMeta, + )..add(const RowBannerEvent.initial()), + child: MouseRegion( + onEnter: (event) => _isHovering.value = true, + onExit: (event) => _isHovering.value = false, + child: SizedBox( + height: 80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 30, + child: _BannerAction( + isHovering: _isHovering, + popoverController: popoverController, + ), + ), + _BannerTitle( + cellBuilder: widget.cellBuilder, + popoverController: popoverController, + ), + ], + ), + ), + ), + ); + } +} + +class _BannerAction extends StatelessWidget { + final ValueNotifier isHovering; + final PopoverController popoverController; + const _BannerAction({ + required this.isHovering, + required this.popoverController, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isHovering, + builder: (BuildContext context, bool value, Widget? child) { + if (value) { + return BlocBuilder( + builder: (context, state) { + final children = []; + final rowMeta = state.rowMeta; + if (rowMeta.icon.isEmpty) { + children.add( + EmojiPickerButton( + showEmojiPicker: () => popoverController.show(), + ), + ); + } else { + children.add( + RemoveEmojiButton( + onRemoved: () { + context + .read() + .add(const RowBannerEvent.setIcon('')); + }, + ), + ); + } + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + }, + ); + } else { + return const SizedBox(height: _kBannerActionHeight); + } + }, + ); + } +} + +class _BannerTitle extends StatefulWidget { + final RowBannerCellBuilder cellBuilder; + final PopoverController popoverController; + const _BannerTitle({ + required this.cellBuilder, + required this.popoverController, + }); + + @override + State<_BannerTitle> createState() => _BannerTitleState(); +} + +class _BannerTitleState extends State<_BannerTitle> { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final children = []; + + if (state.rowMeta.icon.isNotEmpty) { + children.add( + EmojiButton( + emoji: state.rowMeta.icon, + showEmojiPicker: () => widget.popoverController.show(), + ), + ); + } + + if (state.primaryField != null) { + children.add( + Expanded( + child: widget.cellBuilder(state.primaryField!.id), + ), + ); + } + + return AppFlowyPopover( + controller: widget.popoverController, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (popoverContext) => _buildEmojiPicker((emoji) { + context + .read() + .add(RowBannerEvent.setIcon(emoji.emoji)); + widget.popoverController.close(); + }), + child: Row(children: children), + ); + }, + ); + } +} + +typedef OnSubmittedEmoji = void Function(Emoji emoji); +const _kBannerActionHeight = 40.0; + +class EmojiButton extends StatelessWidget { + final String emoji; + final VoidCallback showEmojiPicker; + + const EmojiButton({ + required this.emoji, + required this.showEmojiPicker, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: _kBannerActionHeight, + width: _kBannerActionHeight, + child: FlowyButton( + margin: const EdgeInsets.all(4), + text: FlowyText.medium( + emoji, + fontSize: 30, + textAlign: TextAlign.center, + ), + onTap: showEmojiPicker, + ), + ); + } +} + +class EmojiPickerButton extends StatefulWidget { + final VoidCallback showEmojiPicker; + const EmojiPickerButton({ + super.key, + required this.showEmojiPicker, + }); + + @override + State createState() => _EmojiPickerButtonState(); +} + +class _EmojiPickerButtonState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 160, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ), + leftIcon: const Icon( + Icons.emoji_emotions, + size: 16, + ), + onTap: widget.showEmojiPicker, + ), + ); + } +} + +class RemoveEmojiButton extends StatelessWidget { + final VoidCallback onRemoved; + RemoveEmojiButton({ + super.key, + required this.onRemoved, + }); + + final popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + width: 160, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + leftIcon: const Icon( + Icons.emoji_emotions, + size: 16, + ), + onTap: onRemoved, + ), + ); + } +} + +Widget _buildEmojiPicker(OnSubmittedEmoji onSubmitted) { + return SizedBox( + height: 250, + child: EmojiSelectionMenu( + onSubmitted: onSubmitted, + onExit: () {}, + ), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index b3897942bd..93eb9582db 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -1,31 +1,20 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; -import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; -import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:collection/collection.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/image.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import '../../grid/presentation/layout/sizes.dart'; -import 'accessory/cell_accessory.dart'; import 'cell_builder.dart'; -import 'cells/date_cell/date_cell.dart'; -import 'cells/select_option_cell/select_option_cell.dart'; import 'cells/text_cell/text_cell.dart'; -import 'cells/url_cell/url_cell.dart'; -import '../../grid/presentation/widgets/header/field_cell.dart'; -import '../../grid/presentation/widgets/header/field_editor.dart'; +import 'row_action.dart'; +import 'row_banner.dart'; +import 'row_property.dart'; class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { final RowController rowController; @@ -46,6 +35,14 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { } class _RowDetailPageState extends State { + final scrollController = ScrollController(); + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return FlowyDialog( @@ -55,43 +52,91 @@ class _RowDetailPageState extends State { ..add(const RowDetailEvent.initial()); }, child: ListView( + controller: scrollController, children: [ - // using ListView here for future expansion: - // - header and cover image - // - lower rich text area + _rowBanner(), IntrinsicHeight(child: _responsiveRowInfo()), const Divider(height: 1.0), - const SizedBox(height: 10), + const VSpace(10), + RowDocument( + viewId: widget.rowController.viewId, + rowId: widget.rowController.rowId, + scrollController: scrollController, + ), ], ), ), ); } + Widget _rowBanner() { + return BlocBuilder( + builder: (context, state) { + final paddingOffset = getHorizontalPadding(context); + return Padding( + padding: EdgeInsets.only( + left: paddingOffset, + right: paddingOffset, + top: 20, + ), + child: RowBanner( + rowMeta: widget.rowController.rowMeta, + viewId: widget.rowController.viewId, + cellBuilder: (fieldId) { + final fieldInfo = state.cells + .firstWhereOrNull( + (e) => e.fieldInfo.field.id == fieldId, + ) + ?.fieldInfo; + + if (fieldInfo != null) { + final style = GridTextCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + textStyle: Theme.of(context).textTheme.titleLarge, + showEmoji: false, + autofocus: true, + ); + final cellContext = DatabaseCellContext( + viewId: widget.rowController.viewId, + rowMeta: widget.rowController.rowMeta, + fieldInfo: fieldInfo, + ); + return widget.cellBuilder.build(cellContext, style: style); + } else { + return const SizedBox.shrink(); + } + }, + ), + ); + }, + ); + } + Widget _responsiveRowInfo() { - final rowDataColumn = _PropertyColumn( + final rowDataColumn = RowPropertyList( cellBuilder: widget.cellBuilder, viewId: widget.rowController.viewId, ); - final rowOptionColumn = _RowOptionColumn( + final rowOptionColumn = RowActionList( viewId: widget.rowController.viewId, rowController: widget.rowController, ); + final paddingOffset = getHorizontalPadding(context); if (MediaQuery.of(context).size.width > 800) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( - flex: 4, + flex: 3, child: Padding( - padding: const EdgeInsets.fromLTRB(50, 50, 20, 20), + padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20), child: rowDataColumn, ), ), const VerticalDivider(width: 1.0), Flexible( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 50, 20, 20), + padding: EdgeInsets.fromLTRB(20, 0, paddingOffset, 0), child: rowOptionColumn, ), ), @@ -103,12 +148,12 @@ class _RowDetailPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.fromLTRB(20, 50, 20, 20), + padding: EdgeInsets.fromLTRB(paddingOffset, 0, 20, 20), child: rowDataColumn, ), const Divider(height: 1.0), Padding( - padding: const EdgeInsets.all(20), + padding: EdgeInsets.symmetric(horizontal: paddingOffset), child: rowOptionColumn, ) ], @@ -117,352 +162,10 @@ class _RowDetailPageState extends State { } } -class _PropertyColumn extends StatelessWidget { - final String viewId; - final GridCellBuilder cellBuilder; - const _PropertyColumn({ - required this.viewId, - required this.cellBuilder, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.gridCells != current.gridCells, - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _RowTitle( - cellContext: state.gridCells - .firstWhereOrNull((e) => e.fieldInfo.isPrimary), - cellBuilder: cellBuilder, - ), - const VSpace(20), - ...state.gridCells - .where((element) => !element.fieldInfo.isPrimary) - .map( - (cell) => Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: _PropertyCell( - cellContext: cell, - cellBuilder: cellBuilder, - ), - ), - ) - .toList(), - const VSpace(20), - _CreatePropertyButton(viewId: viewId), - ], - ); - }, - ); - } -} - -class _RowTitle extends StatelessWidget { - final DatabaseCellContext? cellContext; - final GridCellBuilder cellBuilder; - const _RowTitle({this.cellContext, required this.cellBuilder, Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - if (cellContext == null) { - return const SizedBox(); - } - final style = GridTextCellStyle( - placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - textStyle: Theme.of(context).textTheme.titleLarge, - autofocus: true, - ); - return cellBuilder.build(cellContext!, style: style); - } -} - -class _CreatePropertyButton extends StatefulWidget { - final String viewId; - - const _CreatePropertyButton({ - required this.viewId, - Key? key, - }) : super(key: key); - - @override - State<_CreatePropertyButton> createState() => _CreatePropertyButtonState(); -} - -class _CreatePropertyButtonState extends State<_CreatePropertyButton> { - late PopoverController popoverController; - late TypeOptionPB typeOption; - - @override - void initState() { - popoverController = PopoverController(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(240, 200)), - controller: popoverController, - direction: PopoverDirection.topWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - child: SizedBox( - height: 40, - child: FlowyButton( - text: FlowyText.medium( - LocaleKeys.grid_field_newProperty.tr(), - color: AFThemeExtension.of(context).textColor, - ), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: () async { - final result = await TypeOptionBackendService.createFieldTypeOption( - viewId: widget.viewId, - ); - result.fold( - (l) { - typeOption = l; - popoverController.show(); - }, - (r) => Log.error("Failed to create field type option: $r"), - ); - }, - leftIcon: svgWidget( - "home/add", - color: AFThemeExtension.of(context).textColor, - ), - ), - ), - popupBuilder: (BuildContext popOverContext) { - return FieldEditor( - viewId: widget.viewId, - typeOptionLoader: FieldTypeOptionLoader( - viewId: widget.viewId, - field: typeOption.field_2, - ), - onDeleted: (fieldId) { - popoverController.close(); - NavigatorAlertDialog( - title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), - confirm: () { - context - .read() - .add(RowDetailEvent.deleteField(fieldId)); - }, - ).show(context); - }, - ); - }, - ); - } -} - -class _PropertyCell extends StatefulWidget { - final DatabaseCellContext cellContext; - final GridCellBuilder cellBuilder; - const _PropertyCell({ - required this.cellContext, - required this.cellBuilder, - Key? key, - }) : super(key: key); - - @override - State createState() => _PropertyCellState(); -} - -class _PropertyCellState extends State<_PropertyCell> { - final PopoverController popover = PopoverController(); - - @override - Widget build(BuildContext context) { - final style = _customCellStyle(widget.cellContext.fieldType); - final cell = widget.cellBuilder.build(widget.cellContext, style: style); - - final gesture = GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => cell.beginFocus.notify(), - child: AccessoryHover( - contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3), - child: cell, - ), - ); - - return IntrinsicHeight( - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30), - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AppFlowyPopover( - controller: popover, - constraints: BoxConstraints.loose(const Size(240, 600)), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (popoverContext) => buildFieldEditor(), - child: SizedBox( - width: 150, - child: FieldCellButton( - field: widget.cellContext.fieldInfo.field, - onTap: () => popover.show(), - radius: BorderRadius.circular(6), - ), - ), - ), - const HSpace(10), - Expanded(child: gesture), - ], - ), - ), - ); - } - - Widget buildFieldEditor() { - return FieldEditor( - viewId: widget.cellContext.viewId, - isGroupingField: widget.cellContext.fieldInfo.isGroupField, - typeOptionLoader: FieldTypeOptionLoader( - viewId: widget.cellContext.viewId, - field: widget.cellContext.fieldInfo.field, - ), - onHidden: (fieldId) { - popover.close(); - context.read().add(RowDetailEvent.hideField(fieldId)); - }, - onDeleted: (fieldId) { - popover.close(); - - NavigatorAlertDialog( - title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), - confirm: () { - context - .read() - .add(RowDetailEvent.deleteField(fieldId)); - }, - ).show(context); - }, - ); - } -} - -GridCellStyle? _customCellStyle(FieldType fieldType) { - switch (fieldType) { - case FieldType.Checkbox: - return null; - case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - return DateCellStyle( - alignment: Alignment.centerLeft, - ); - case FieldType.MultiSelect: - return SelectOptionCellStyle( - placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - ); - case FieldType.Checklist: - return SelectOptionCellStyle( - placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - ); - case FieldType.Number: - return null; - case FieldType.RichText: - return GridTextCellStyle( - placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - ); - case FieldType.SingleSelect: - return SelectOptionCellStyle( - placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - ); - - case FieldType.URL: - return GridURLCellStyle( - placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - accessoryTypes: [ - GridURLCellAccessoryType.copyURL, - GridURLCellAccessoryType.visitURL, - ], - ); - } - throw UnimplementedError; -} - -class _RowOptionColumn extends StatelessWidget { - final RowController rowController; - const _RowOptionColumn({ - required String viewId, - required this.rowController, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: FlowyText(LocaleKeys.grid_row_action.tr()), - ), - const VSpace(15), - _DeleteButton(rowId: rowController.rowId), - _DuplicateButton( - rowId: rowController.rowId, - groupId: rowController.groupId, - ), - ], - ); - } -} - -class _DeleteButton extends StatelessWidget { - final String rowId; - const _DeleteButton({required this.rowId, Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()), - leftIcon: const FlowySvg(name: "home/trash"), - onTap: () { - context.read().add(RowDetailEvent.deleteRow(rowId)); - FlowyOverlay.pop(context); - }, - ), - ); - } -} - -class _DuplicateButton extends StatelessWidget { - final String rowId; - final String? groupId; - const _DuplicateButton({ - required this.rowId, - this.groupId, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()), - leftIcon: const FlowySvg(name: "grid/duplicate"), - onTap: () { - context - .read() - .add(RowDetailEvent.duplicateRow(rowId, groupId)); - FlowyOverlay.pop(context); - }, - ), - ); +double getHorizontalPadding(BuildContext context) { + if (MediaQuery.of(context).size.width > 800) { + return 50; + } else { + return 20; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart new file mode 100644 index 0000000000..9aa55963d1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_document.dart @@ -0,0 +1,120 @@ +import 'package:appflowy/plugins/database_view/grid/application/row/row_document_bloc.dart'; +import 'package:appflowy/plugins/document/application/doc_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class RowDocument extends StatelessWidget { + const RowDocument({ + super.key, + required this.viewId, + required this.rowId, + required this.scrollController, + }); + + final String viewId; + final String rowId; + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RowDocumentBloc( + viewId: viewId, + rowId: rowId, + )..add( + const RowDocumentEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + error: (error) => FlowyErrorPage( + error.toString(), + ), + finish: () => RowEditor( + viewPB: state.viewPB!, + scrollController: scrollController, + ), + ); + }, + ), + ); + } +} + +class RowEditor extends StatefulWidget { + const RowEditor({ + super.key, + required this.viewPB, + required this.scrollController, + }); + + final ViewPB viewPB; + final ScrollController scrollController; + + @override + State createState() => _RowEditorState(); +} + +class _RowEditorState extends State { + late final DocumentBloc documentBloc; + + @override + void initState() { + super.initState(); + documentBloc = DocumentBloc(view: widget.viewPB) + ..add(const DocumentEvent.initial()); + } + + @override + dispose() { + documentBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => DocumentAppearanceCubit()), + BlocProvider.value(value: documentBloc), + ], + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + finish: (result) { + return result.fold( + (error) => FlowyErrorPage( + error.toString(), + ), + (_) { + final editorState = documentBloc.editorState; + if (editorState == null) { + return const SizedBox.shrink(); + } + return IntrinsicHeight( + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, + editorState: editorState, + scrollController: widget.scrollController, + ), + ); + }, + ); + }, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart new file mode 100644 index 0000000000..19bb71f064 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -0,0 +1,192 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'accessory/cell_accessory.dart'; +import 'cell_builder.dart'; +import 'cells/date_cell/date_cell.dart'; +import 'cells/select_option_cell/select_option_cell.dart'; +import 'cells/text_cell/text_cell.dart'; +import 'cells/url_cell/url_cell.dart'; + +/// Display the row properties in a list. Only use this widget in the +/// [RowDetailPage]. +/// +class RowPropertyList extends StatelessWidget { + final String viewId; + final GridCellBuilder cellBuilder; + const RowPropertyList({ + required this.viewId, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.cells != current.cells, + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // The rest of the fields are displayed in the order of the field + // list + ...state.cells + .where((element) => !element.fieldInfo.isPrimary) + .map( + (cell) => _PropertyCell( + cellContext: cell, + cellBuilder: cellBuilder, + ), + ) + .toList(), + const VSpace(20), + + // Create a new property(field) button + CreateRowFieldButton(viewId: viewId), + ], + ); + }, + ); + } +} + +class _PropertyCell extends StatefulWidget { + final DatabaseCellContext cellContext; + final GridCellBuilder cellBuilder; + const _PropertyCell({ + required this.cellContext, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _PropertyCellState(); +} + +class _PropertyCellState extends State<_PropertyCell> { + final PopoverController popover = PopoverController(); + + @override + Widget build(BuildContext context) { + final style = _customCellStyle(widget.cellContext.fieldType); + final cell = widget.cellBuilder.build(widget.cellContext, style: style); + + final gesture = GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => cell.beginFocus.notify(), + child: AccessoryHover( + contentPadding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3), + child: cell, + ), + ); + + return IntrinsicHeight( + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AppFlowyPopover( + controller: popover, + constraints: BoxConstraints.loose(const Size(240, 600)), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + popupBuilder: (popoverContext) => buildFieldEditor(), + child: SizedBox( + width: 150, + child: FieldCellButton( + field: widget.cellContext.fieldInfo.field, + onTap: () => popover.show(), + radius: BorderRadius.circular(6), + ), + ), + ), + Expanded(child: gesture), + ], + ), + ), + ); + } + + Widget buildFieldEditor() { + return FieldEditor( + viewId: widget.cellContext.viewId, + isGroupingField: widget.cellContext.fieldInfo.isGroupField, + typeOptionLoader: FieldTypeOptionLoader( + viewId: widget.cellContext.viewId, + field: widget.cellContext.fieldInfo.field, + ), + onHidden: (fieldId) { + popover.close(); + context.read().add(RowDetailEvent.hideField(fieldId)); + }, + onDeleted: (fieldId) { + popover.close(); + + NavigatorAlertDialog( + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + confirm: () { + context + .read() + .add(RowDetailEvent.deleteField(fieldId)); + }, + ).show(context); + }, + ); + } +} + +GridCellStyle? _customCellStyle(FieldType fieldType) { + switch (fieldType) { + case FieldType.Checkbox: + return null; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateCellStyle( + alignment: Alignment.centerLeft, + ); + case FieldType.MultiSelect: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.Checklist: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.Number: + return null; + case FieldType.RichText: + return GridTextCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + case FieldType.SingleSelect: + return SelectOptionCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); + + case FieldType.URL: + return GridURLCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + accessoryTypes: [ + GridURLCellAccessoryType.copyURL, + GridURLCellAccessoryType.visitURL, + ], + ); + } + throw UnimplementedError; +} 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 f8a50b647d..5ae82dc390 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 @@ -23,7 +23,7 @@ class DatabaseSettingList extends StatelessWidget { Widget build(BuildContext context) { final cells = actionsForDatabaseLayout(databaseContoller.databaseLayout) .map((action) { - return _SettingItem( + return DatabaseSettingItem( action: action, onAction: (action) => onAction(action, databaseContoller), ); @@ -44,11 +44,11 @@ class DatabaseSettingList extends StatelessWidget { } } -class _SettingItem extends StatelessWidget { +class DatabaseSettingItem extends StatelessWidget { final DatabaseSettingAction action; final Function(DatabaseSettingAction) onAction; - const _SettingItem({ + const DatabaseSettingItem({ required this.action, required this.onAction, Key? key, diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 8f2b77b591..8c82db14bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -23,7 +23,7 @@ class DocumentBloc extends Bloc { DocumentBloc({ required this.view, }) : _documentListener = DocumentListener(id: view.id), - _viewListener = ViewListener(view: view), + _viewListener = ViewListener(viewId: view.id), _documentService = DocumentService(), _trashService = TrashService(), super(DocumentState.initial()) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 34cea94b75..fd92994994 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -48,8 +48,12 @@ class DocumentPlugin extends Plugin { DocumentPlugin({ required PluginType pluginType, required ViewPB view, + bool listenOnViewChanged = false, Key? key, - }) : notifier = ViewPluginNotifier(view: view) { + }) : notifier = ViewPluginNotifier( + view: view, + listenOnViewChanged: listenOnViewChanged, + ) { _pluginType = pluginType; _documentAppearanceCubit.fetch(); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 892a4857e9..c26369e1ed 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -14,17 +14,23 @@ class AppFlowyEditorPage extends StatefulWidget { super.key, required this.editorState, this.header, + this.shrinkWrap = false, + this.scrollController, + this.autoFocus, }); - final EditorState editorState; final Widget? header; + final EditorState editorState; + final ScrollController? scrollController; + final bool shrinkWrap; + final bool? autoFocus; @override State createState() => _AppFlowyEditorPageState(); } class _AppFlowyEditorPageState extends State { - final scrollController = ScrollController(); + late final ScrollController effectiveScrollController; final List commandShortcutEvents = [ ...codeBlockCommands, @@ -90,6 +96,20 @@ class _AppFlowyEditorPageState extends State { ); DocumentBloc get documentBloc => context.read(); + @override + void initState() { + super.initState(); + effectiveScrollController = widget.scrollController ?? ScrollController(); + } + + @override + void dispose() { + if (widget.scrollController == null) { + effectiveScrollController.dispose(); + } + super.dispose(); + } + @override Widget build(BuildContext context) { final (bool autoFocus, Selection? selection) = @@ -98,9 +118,10 @@ class _AppFlowyEditorPageState extends State { final editor = AppFlowyEditor.custom( editorState: widget.editorState, editable: true, - scrollController: scrollController, + shrinkWrap: widget.shrinkWrap, + scrollController: effectiveScrollController, // setup the auto focus parameters - autoFocus: autoFocus, + autoFocus: widget.autoFocus ?? autoFocus, focusedSelection: selection, // setup the theme editorStyle: styleCustomizer.style(), @@ -122,7 +143,7 @@ class _AppFlowyEditorPageState extends State { style: styleCustomizer.floatingToolbarStyleBuilder(), items: toolbarItems, editorState: widget.editorState, - scrollController: scrollController, + scrollController: effectiveScrollController, child: editor, ), ), 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 2b17919788..25aa43ab77 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 @@ -61,6 +61,7 @@ extension InsertDatabase on EditorState { ).then((value) => value.swap().toOption().toNullable()); // TODO(a-wallen): Show error dialog here. + // Maybe extend the FlowyErrorPage. if (ref == null) { return; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart index 3aba387cbc..162315a5e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -84,7 +84,7 @@ class ShareActionList extends StatefulWidget { @visibleForTesting class ShareActionListState extends State { late String name; - late final ViewListener viewListener = ViewListener(view: widget.view); + late final ViewListener viewListener = ViewListener(viewId: widget.view.id); @override void initState() { @@ -134,7 +134,7 @@ class ShareActionListState extends State { name = widget.view.name; viewListener.start( onViewUpdated: (view) { - name = view.fold((l) => l.name, (r) => ''); + name = view.name; }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/util.dart b/frontend/appflowy_flutter/lib/plugins/util.dart index 9bc4b8dd87..f64fe56a75 100644 --- a/frontend/appflowy_flutter/lib/plugins/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/util.dart @@ -1,10 +1,14 @@ 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; @@ -12,35 +16,37 @@ class ViewPluginNotifier extends PluginNotifier> { @override final ValueNotifier> isDeleted = ValueNotifier(none()); - @override - final ValueNotifier isDisplayChanged = ValueNotifier(0); - ViewPluginNotifier({ required this.view, - }) : _viewListener = ViewListener(view: view) { - _viewListener?.start( - onViewUpdated: (result) { - result.fold( - (updatedView) { + 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; - isDisplayChanged.value = updatedView.hashCode; - }, - (err) => Log.error(err), - ); - }, - onViewMoveToTrash: (result) { - result.fold( - (deletedView) => isDeleted.value = some(deletedView), - (err) => Log.error(err), - ); - }, - ); + } + }, + onViewMoveToTrash: (result) { + result.fold( + (deletedView) => isDeleted.value = some(deletedView), + (err) => Log.error(err), + ); + }, + ); + } } @override void dispose() { isDeleted.dispose(); - isDisplayChanged.dispose(); _viewListener?.stop(); } } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index cd61f90f78..391ff6997c 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -122,11 +122,6 @@ void _resolveFolderDeps(GetIt getIt) { WorkspaceListener(user: user, workspaceId: workspaceId), ); - // ViewPB - getIt.registerFactoryParam( - (view, _) => ViewListener(view: view), - ); - getIt.registerFactoryParam( (view, _) => ViewBloc( view: view, diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index 2ea7ce5e0a..28c38f5ead 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -37,9 +37,6 @@ abstract class PluginNotifier { /// Notify if the plugin get deleted ValueNotifier get isDeleted; - /// Notify if the [PluginWidgetBuilder]'s content was changed - ValueNotifier get isDisplayChanged; - void dispose() {} } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index bdf623c9fb..f4f4e2ea20 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -16,14 +16,14 @@ class ViewBloc extends Bloc { ViewBloc({ required this.view, }) : viewBackendSvc = ViewBackendService(), - listener = ViewListener(view: view), + listener = ViewListener(viewId: view.id), super(ViewState.init(view)) { on((event, emit) async { await event.map( initial: (e) { listener.start( onViewUpdated: (result) { - add(ViewEvent.viewDidUpdate(result)); + add(ViewEvent.viewDidUpdate(left(result))); }, ); emit(state); 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 4aa954ce14..0fb2bd20d4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -1,4 +1,7 @@ -import 'package:appflowy/plugins/database_view/database_view.dart'; +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/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:flowy_infra/image.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -56,14 +59,32 @@ extension ViewExtension on ViewPB { throw UnimplementedError; } - Plugin plugin() { + 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 DatabaseViewPlugin(view: this); + return GridPlugin( + view: this, + pluginType: pluginType, + listenOnViewChanged: listenOnViewChanged, + ); case ViewLayoutPB.Document: - return makePlugin(pluginType: pluginType, data: this); + return DocumentPlugin( + view: this, + pluginType: pluginType, + listenOnViewChanged: listenOnViewChanged, + ); } throw UnimplementedError; } 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 b1a34f8767..78159c96b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart @@ -1,18 +1,18 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:appflowy/core/notification/folder_notification.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/notification.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:flowy_infra/notifier.dart'; // Delete the view from trash, which means the view was deleted permanently typedef DeleteViewNotifyValue = Either; // The view get updated -typedef UpdateViewNotifiedValue = Either; +typedef UpdateViewNotifiedValue = ViewPB; // Restore the view from trash typedef RestoreViewNotifiedValue = Either; // Move the view to trash @@ -20,15 +20,17 @@ typedef MoveToTrashNotifiedValue = Either; class ViewListener { StreamSubscription? _subscription; - final _updatedViewNotifier = PublishNotifier(); - final _deletedNotifier = PublishNotifier(); - final _restoredNotifier = PublishNotifier(); - final _moveToTrashNotifier = PublishNotifier(); + void Function(UpdateViewNotifiedValue)? _updatedViewNotifier; + void Function(DeleteViewNotifyValue)? _deletedNotifier; + void Function(RestoreViewNotifiedValue)? _restoredNotifier; + void Function(MoveToTrashNotifiedValue)? _moveToTrashNotifier; + bool _isDisposed = false; + FolderNotificationParser? _parser; - ViewPB view; + final String viewId; ViewListener({ - required this.view, + required this.viewId, }); void start({ @@ -37,32 +39,18 @@ class ViewListener { void Function(RestoreViewNotifiedValue)? onViewRestored, void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash, }) { - if (onViewUpdated != null) { - _updatedViewNotifier.addListener(() { - onViewUpdated(_updatedViewNotifier.currentValue!); - }); + if (_isDisposed) { + Log.warn("ViewListener is already disposed"); + return; } - if (onViewDeleted != null) { - _deletedNotifier.addListener(() { - onViewDeleted(_deletedNotifier.currentValue!); - }); - } - - if (onViewRestored != null) { - _restoredNotifier.addListener(() { - onViewRestored(_restoredNotifier.currentValue!); - }); - } - - if (onViewMoveToTrash != null) { - _moveToTrashNotifier.addListener(() { - onViewMoveToTrash(_moveToTrashNotifier.currentValue!); - }); - } + _updatedViewNotifier = onViewUpdated; + _deletedNotifier = onViewDeleted; + _restoredNotifier = onViewRestored; + _moveToTrashNotifier = onViewMoveToTrash; _parser = FolderNotificationParser( - id: view.id, + id: viewId, callback: (ty, result) { _handleObservableType(ty, result); }, @@ -81,30 +69,29 @@ class ViewListener { result.fold( (payload) { final view = ViewPB.fromBuffer(payload); - _updatedViewNotifier.value = left(view); + _updatedViewNotifier?.call(view); }, - (error) => _updatedViewNotifier.value = right(error), + (error) => Log.error(error), ); break; case FolderNotification.DidDeleteView: result.fold( - (payload) => - _deletedNotifier.value = left(ViewPB.fromBuffer(payload)), - (error) => _deletedNotifier.value = right(error), + (payload) => _deletedNotifier?.call(left(ViewPB.fromBuffer(payload))), + (error) => _deletedNotifier?.call(right(error)), ); break; case FolderNotification.DidRestoreView: result.fold( (payload) => - _restoredNotifier.value = left(ViewPB.fromBuffer(payload)), - (error) => _restoredNotifier.value = right(error), + _restoredNotifier?.call(left(ViewPB.fromBuffer(payload))), + (error) => _restoredNotifier?.call(right(error)), ); break; case FolderNotification.DidMoveViewToTrash: result.fold( - (payload) => _moveToTrashNotifier.value = - left(DeletedViewPB.fromBuffer(payload)), - (error) => _moveToTrashNotifier.value = right(error), + (payload) => _moveToTrashNotifier + ?.call(left(DeletedViewPB.fromBuffer(payload))), + (error) => _moveToTrashNotifier?.call(right(error)), ); break; default: @@ -113,10 +100,11 @@ class ViewListener { } Future stop() async { + _isDisposed = true; _parser = null; await _subscription?.cancel(); - _updatedViewNotifier.dispose(); - _deletedNotifier.dispose(); - _restoredNotifier.dispose(); + _updatedViewNotifier = null; + _deletedNotifier = null; + _restoredNotifier = null; } } 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 58605ab462..42d046e849 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -50,6 +50,29 @@ class ViewBackendService { return FolderEventCreateView(payload).send(); } + /// The orphan view is meant to be a view that is not attached to any parent view. By default, this + /// view will not be shown in the view list unless it is attached to a parent view that is shown in + /// the view list. + static Future> createOrphanView({ + required String viewId, + required ViewLayoutPB layoutType, + required String name, + String? desc, + + /// The initial data should be a JSON that represent the DocumentDataPB. + /// Currently, only support create document with initial data. + List? initialDataBytes, + }) { + final payload = CreateOrphanViewPayloadPB.create() + ..viewId = viewId + ..name = name + ..desc = desc ?? "" + ..layout = layoutType + ..initialData = initialDataBytes ?? []; + + return FolderEventCreateOrphanView(payload).send(); + } + static Future> createDatabaseReferenceView({ required String parentViewId, required String databaseId, @@ -98,12 +121,23 @@ class ViewBackendService { static Future> updateView({ required String viewId, String? name, + String? iconURL, + String? coverURL, }) { final payload = UpdateViewPayloadPB.create()..viewId = viewId; if (name != null) { payload.name = name; } + + if (iconURL != null) { + payload.iconUrl = iconURL; + } + + if (coverURL != null) { + payload.coverUrl = coverURL; + } + return FolderEventUpdateView(payload).send(); } @@ -144,7 +178,7 @@ class ViewBackendService { }); } - Future> getView( + static Future> getView( String viewID, ) async { final payload = ViewIdPB.create()..value = viewID; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart index 4998d4ba4e..392cf22b7e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart @@ -78,11 +78,9 @@ class _HomeScreenState extends State { // All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash. if (getIt().plugin.pluginType == PluginType.blank) { - final plugin = makePlugin( - pluginType: view.pluginType, - data: view, + getIt().setPlugin( + view.plugin(listenOnViewChanged: true), ); - getIt().setPlugin(plugin); getIt().latestOpenView = view; } } @@ -282,12 +280,10 @@ class HomeScreenStackAdaptor extends HomeStackDelegate { lastView = views[index - 1]; } - final plugin = makePlugin( - pluginType: lastView.pluginType, - data: lastView, - ); getIt().latestOpenView = lastView; - getIt().setPlugin(plugin); + getIt().setPlugin( + lastView.plugin(listenOnViewChanged: true), + ); } else { getIt().latestOpenView = null; getIt().setPlugin(BlankPagePlugin()); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 7155c4c385..e47eb8820c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -121,14 +121,12 @@ class HomeStackNotifier extends ChangeNotifier { /// This is the only place where the plugin is set. /// No need compare the old plugin with the new plugin. Just set it. set plugin(Plugin newPlugin) { - _plugin.notifier?.isDisplayChanged.addListener(notifyListeners); _plugin.dispose(); /// Set the plugin view as the latest view. FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); _plugin = newPlugin; - _plugin.notifier?.isDisplayChanged.removeListener(notifyListeners); notifyListeners(); } 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 3aab3b1863..20788159de 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 @@ -27,7 +27,9 @@ class ViewSection extends StatelessWidget { listener: (context, state) { if (state.selectedView != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - getIt().setPlugin(state.selectedView!.plugin()); + getIt().setPlugin( + state.selectedView!.plugin(listenOnViewChanged: true), + ); }); } }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart index 95c23eb04e..7aeed651af 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart @@ -1,6 +1,5 @@ import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; @@ -25,20 +24,15 @@ class _ViewLeftBarItemState extends State { super.initState(); view = widget.view; _focusNode.addListener(_handleFocusChanged); - _viewListener = ViewListener(view: widget.view); + _viewListener = ViewListener(viewId: widget.view.id); _viewListener.start( - onViewUpdated: (result) { - result.fold( - (updatedView) { - if (mounted) { - setState(() { - view = updatedView; - _controller.text = view.name; - }); - } - }, - (err) => Log.error(err), - ); + onViewUpdated: (updatedView) { + if (mounted) { + setState(() { + view = updatedView; + _controller.text = view.name; + }); + } }, ); _controller.text = view.name; diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index a59affefc0..69455a0985 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,11 @@ packages: dependency: "direct main" description: path: "." - ref: "7fe5bb8" - resolved-ref: "7fe5bb85d455416ddbce4bbf2afed1c434466eeb" + ref: "23bc6d2" + resolved-ref: "23bc6d2f58ab7ab4ff21c507d53753de35094ec0" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "1.0.0" + version: "1.0.2" appflowy_popover: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index b05b57af95..023ece27c1 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -42,11 +42,11 @@ dependencies: git: url: https://github.com/AppFlowy-IO/appflowy-board.git ref: a183c57 - # appflowy_editor: ^1.0.0 + # appflowy_editor: ^1.0.2 appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 7fe5bb8 + ref: 23bc6d2 appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index 2cedbd7716..33880ba2bd 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -106,13 +106,14 @@ class BoardTestContext { final rowDataController = RowController( viewId: rowInfo.viewId, - rowId: rowInfo.rowPB.id, + rowMeta: rowInfo.rowMeta, rowCache: rowCache, ); final rowBloc = RowBloc( - rowInfo: rowInfo, + viewId: rowInfo.viewId, dataController: rowDataController, + rowId: rowInfo.rowMeta.id, )..add(const RowEvent.initial()); await gridResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart index 678036798b..e7fa5cc464 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart @@ -63,16 +63,16 @@ void main() { act: (bloc) async { await gridResponseFuture(); - firstId = bloc.state.rowInfos[0].rowPB.id; - secondId = bloc.state.rowInfos[1].rowPB.id; - thirdId = bloc.state.rowInfos[2].rowPB.id; + firstId = bloc.state.rowInfos[0].rowId; + secondId = bloc.state.rowInfos[1].rowId; + thirdId = bloc.state.rowInfos[2].rowId; bloc.add(const GridEvent.moveRow(0, 2)); }, verify: (bloc) { - expect(secondId, bloc.state.rowInfos[0].rowPB.id); - expect(thirdId, bloc.state.rowInfos[1].rowPB.id); - expect(firstId, bloc.state.rowInfos[2].rowPB.id); + expect(secondId, bloc.state.rowInfos[0].rowId); + expect(thirdId, bloc.state.rowInfos[1].rowId); + expect(firstId, bloc.state.rowInfos[2].rowId); }, ); }); 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 144cbe8bd4..9471afcbf1 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -35,7 +35,7 @@ class GridTestContext { return gridController.fieldController; } - Future> createRow() async { + Future> createRow() async { return gridController.createRow(); } @@ -55,14 +55,15 @@ class GridTestContext { final rowCache = gridController.rowCache; final rowDataController = RowController( - rowId: rowInfo.rowPB.id, + rowMeta: rowInfo.rowMeta, viewId: rowInfo.viewId, rowCache: rowCache, ); final rowBloc = RowBloc( - rowInfo: rowInfo, + viewId: rowInfo.viewId, dataController: rowDataController, + rowId: rowInfo.rowMeta.id, )..add(const RowEvent.initial()); await gridResponseFuture(); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 4175d20961..5d0d094eac 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "collab", @@ -1024,7 +1024,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "bytes", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "bytes", "collab-sync", @@ -1060,7 +1060,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "async-trait", @@ -1086,7 +1086,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "proc-macro2", "quote", @@ -1098,7 +1098,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "collab", @@ -1109,13 +1109,14 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tokio", "tracing", ] [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "chrono", @@ -1135,7 +1136,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "bincode", "chrono", @@ -1155,7 +1156,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "async-trait", @@ -1186,7 +1187,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "bytes", "collab", @@ -5006,6 +5007,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.6" @@ -6206,6 +6213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom 0.2.9", + "sha1_smol", ] [[package]] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index db71e85582..a059d9ed21 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 = "4f5837" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } +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 = { path = "../../AppFlowy-Collab/collab" } #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts index f23e679a6f..71e498bf9a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/group/group_controller.ts @@ -1,4 +1,11 @@ -import { DatabaseNotification, FlowyError, GroupPB, GroupRowsNotificationPB, RowPB } from '@/services/backend'; +import { + DatabaseNotification, + FlowyError, + GroupPB, + GroupRowsNotificationPB, + RowMetaPB, + RowPB, +} from '@/services/backend'; import { ChangeNotifier } from '$app/utils/change_notifier'; import { None, Ok, Option, Result, Some } from 'ts-results'; import { DatabaseNotificationObserver } from '../notifications/observer'; @@ -7,10 +14,10 @@ import { DatabaseBackendService } from '../database_bd_svc'; export type GroupDataCallbacks = { onRemoveRow: (groupId: string, rowId: string) => void; - onInsertRow: (groupId: string, row: RowPB, index?: number) => void; - onUpdateRow: (groupId: string, row: RowPB) => void; + onInsertRow: (groupId: string, row: RowMetaPB, index?: number) => void; + onUpdateRow: (groupId: string, row: RowMetaPB) => void; - onCreateRow: (groupId: string, row: RowPB) => void; + onCreateRow: (groupId: string, row: RowMetaPB) => void; }; export class DatabaseGroupController { @@ -37,7 +44,7 @@ export class DatabaseGroupController { this.group = group; }; - rowAtIndex = (index: number): Option => { + rowAtIndex = (index: number): Option => { if (this.group.rows.length < index) { return None; } @@ -59,16 +66,16 @@ export class DatabaseGroupController { changeset.inserted_rows.forEach((insertedRow) => { let index: number | undefined = insertedRow.index; if (insertedRow.has_index && this.group.rows.length > insertedRow.index) { - this.group.rows.splice(index, 0, insertedRow.row); + this.group.rows.splice(index, 0, insertedRow.row_meta); } else { index = undefined; - this.group.rows.push(insertedRow.row); + this.group.rows.push(insertedRow.row_meta); } if (insertedRow.is_new) { - this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row); + this.callbacks?.onCreateRow(this.group.group_id, insertedRow.row_meta); } else { - this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row, index); + this.callbacks?.onInsertRow(this.group.group_id, insertedRow.row_meta, index); } }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts index bf5b982a56..8cf7bc6230 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/row/row_cache.ts @@ -7,12 +7,13 @@ import { RowsChangePB, RowsVisibilityChangePB, ReorderSingleRowPB, + RowMetaPB, } from '@/services/backend'; import { ChangeNotifier } from '$app/utils/change_notifier'; import { FieldInfo } from '../field/field_controller'; import { CellCache, CellCacheKey } from '../cell/cell_cache'; import { CellIdentifier } from '../cell/cell_bd_svc'; -import { DatabaseEventGetRow } from '@/services/backend/events/flowy-database2'; +import { DatabaseEventGetRow, DatabaseEventGetRowMeta } from '@/services/backend/events/flowy-database2'; import { None, Option, Some } from 'ts-results'; import { Log } from '$app/utils/log'; @@ -75,7 +76,7 @@ export class RowCache { this.notifier.withChange(RowChangedReason.FieldDidChanged); }; - initializeRows = (rows: RowPB[]) => { + initializeRows = (rows: RowMetaPB[]) => { rows.forEach((rowPB) => { this.rowList.push(this._toRowInfo(rowPB)); }); @@ -106,11 +107,7 @@ export class RowCache { } }; - private _refreshRow = (opRow: OptionalRowPB) => { - if (!opRow.has_row) { - return; - } - const updatedRow = opRow.row; + private _refreshRow = (updatedRow: RowMetaPB) => { const option = this.rowList.getRowWithIndex(updatedRow.id); if (option.some) { const { rowInfo, index } = option.val; @@ -124,7 +121,7 @@ export class RowCache { private _loadRow = (rowId: string) => { const payload = RowIdPB.fromObject({ view_id: this.viewId, row_id: rowId }); - return DatabaseEventGetRow(payload); + return DatabaseEventGetRowMeta(payload); }; private _deleteRows = (rowIds: string[]) => { @@ -138,7 +135,7 @@ export class RowCache { private _insertRows = (rows: InsertedRowPB[]) => { rows.forEach((insertedRow) => { - const rowInfo = this._toRowInfo(insertedRow.row); + const rowInfo = this._toRowInfo(insertedRow.row_meta); const insertedIndex = this.rowList.insert(insertedRow.index, rowInfo); if (insertedIndex !== undefined) { this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId); @@ -154,11 +151,11 @@ export class RowCache { const rowInfos: RowInfo[] = []; updatedRows.forEach((updatedRow) => { updatedRow.field_ids.forEach((fieldId) => { - const key = new CellCacheKey(fieldId, updatedRow.row.id); + const key = new CellCacheKey(fieldId, updatedRow.row_meta.id); this.cellCache.remove(key); }); - rowInfos.push(this._toRowInfo(updatedRow.row)); + rowInfos.push(this._toRowInfo(updatedRow.row_meta)); }); const updatedIndexs = this.rowList.insertRows(rowInfos); @@ -178,7 +175,7 @@ export class RowCache { private _displayRows = (insertedRows: InsertedRowPB[]) => { insertedRows.forEach((insertedRow) => { - const insertedIndex = this.rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row)); + const insertedIndex = this.rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row_meta)); if (insertedIndex !== undefined) { this.notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId); @@ -190,7 +187,7 @@ export class RowCache { this.notifier.dispose(); }; - private _toRowInfo = (rowPB: RowPB) => { + private _toRowInfo = (rowPB: RowMetaPB) => { return new RowInfo(this.viewId, this.getFieldInfos(), rowPB); }; @@ -338,10 +335,10 @@ export class RowInfo { constructor( public readonly viewId: string, public readonly fieldInfos: readonly FieldInfo[], - public readonly row: RowPB + public readonly row: RowMetaPB ) {} - copyWith = (params: { row?: RowPB; fieldInfos?: readonly FieldInfo[] }) => { + copyWith = (params: { row?: RowMetaPB; fieldInfos?: readonly FieldInfo[] }) => { return new RowInfo(this.viewId, params.fieldInfos || this.fieldInfos, params.row || this.row); }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts index 1448a7e7e7..e099d3ba6b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/view/database_view_cache.ts @@ -1,7 +1,7 @@ import { DatabaseViewRowsObserver } from './view_row_observer'; import { RowCache, RowInfo } from '../row/row_cache'; import { FieldController } from '../field/field_controller'; -import { RowPB } from '@/services/backend'; +import { RowMetaPB, RowPB } from '@/services/backend'; export class DatabaseViewCache { private readonly rowsObserver: DatabaseViewRowsObserver; @@ -20,7 +20,7 @@ export class DatabaseViewCache { }); } - initializeWithRows = (rows: RowPB[]) => { + initializeWithRows = (rows: RowMetaPB[]) => { this.rowCache.initializeRows(rows); }; diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index e2b6d9e3cb..0c9064f4fa 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=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" 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=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" 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=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" 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=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" 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=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" 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=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "collab", @@ -972,13 +972,14 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tokio", "tracing", ] [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "chrono", @@ -998,7 +999,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "bincode", "chrono", @@ -1018,7 +1019,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "anyhow", "async-trait", @@ -1049,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=4f5837#4f58377a1411745577c216a34672c49dc17413ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e9f7fc#e9f7fc76192f2dbb2379f1a02310047763bd4426" dependencies = [ "bytes", "collab", @@ -4176,6 +4177,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.6" @@ -5029,6 +5036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom 0.2.9", + "sha1_smol", ] [[package]] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 44f073f607..20d766bee6 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 = "4f5837" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "4f5837" } +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 = { path = "../AppFlowy-Collab/collab" } #collab-folder = { path = "../AppFlowy-Collab/collab-folder" } 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 4ca177d481..6277e344f9 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs @@ -2,6 +2,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use crate::entities::parser::NotEmptyStr; +use crate::entities::RowMetaPB; use crate::services::setting::{CalendarLayout, CalendarLayoutSetting}; use super::CellIdPB; @@ -99,7 +100,7 @@ impl TryInto for CalendarEventRequestPB { #[derive(Debug, Clone, Default, ProtoBuf)] pub struct CalendarEventPB { #[pb(index = 1)] - pub row_id: String, + pub row_meta: RowMetaPB, #[pb(index = 2)] pub date_field_id: String, 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 7d3bf8e23d..af0e9ff64e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -6,7 +6,7 @@ use flowy_derive::ProtoBuf; use flowy_error::{ErrorCode, FlowyError}; use crate::entities::parser::NotEmptyStr; -use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowPB}; +use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowMetaPB}; use crate::services::database::CreateDatabaseViewParams; /// [DatabasePB] describes how many fields and blocks the grid has @@ -19,7 +19,7 @@ pub struct DatabasePB { pub fields: Vec, #[pb(index = 3)] - pub rows: Vec, + pub rows: Vec, #[pb(index = 4)] pub layout_type: DatabaseLayoutPB, diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index cdad1eb60c..b2f5e6aa8b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use crate::entities::parser::NotEmptyStr; -use crate::entities::{FieldType, RowPB}; +use crate::entities::{FieldType, RowMetaPB}; use crate::services::group::{GroupChangeset, GroupData, GroupSetting}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -79,7 +79,7 @@ pub struct GroupPB { pub group_name: String, #[pb(index = 4)] - pub rows: Vec, + pub rows: Vec, #[pb(index = 5)] pub is_default: bool, @@ -94,7 +94,11 @@ impl std::convert::From for GroupPB { field_id: group_data.field_id, group_id: group_data.id, group_name: group_data.name, - rows: group_data.rows.into_iter().map(RowPB::from).collect(), + rows: group_data + .rows + .into_iter() + .map(|row_detail| RowMetaPB::from(row_detail.meta)) + .collect(), is_default: group_data.is_default, is_visible: group_data.is_visible, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs index 5b7b99ae6b..59bab13169 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs @@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use crate::entities::parser::NotEmptyStr; -use crate::entities::{GroupPB, InsertedRowPB, RowPB}; +use crate::entities::{GroupPB, InsertedRowPB, RowMetaPB}; #[derive(Debug, Default, ProtoBuf)] pub struct GroupRowsNotificationPB { @@ -21,7 +21,7 @@ pub struct GroupRowsNotificationPB { pub deleted_rows: Vec, #[pb(index = 5)] - pub updated_rows: Vec, + pub updated_rows: Vec, } impl std::fmt::Display for GroupRowsNotificationPB { @@ -29,7 +29,7 @@ impl std::fmt::Display for GroupRowsNotificationPB { for inserted_row in &self.inserted_rows { f.write_fmt(format_args!( "Insert: {} row at {:?}", - inserted_row.row.id, inserted_row.index + inserted_row.row_meta.id, inserted_row.index ))?; } @@ -80,7 +80,7 @@ impl GroupRowsNotificationPB { } } - pub fn update(group_id: String, updated_rows: Vec) -> Self { + pub fn update(group_id: String, updated_rows: Vec) -> Self { Self { group_id, updated_rows, diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 4ce920a2df..e506e35974 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use collab_database::rows::{Row, RowId}; +use collab_database::rows::{Row, RowId, RowMeta}; use collab_database::views::RowOrder; use flowy_derive::ProtoBuf; @@ -36,6 +36,7 @@ impl std::convert::From for RowPB { } } } + impl From for RowPB { fn from(data: RowOrder) -> Self { Self { @@ -45,6 +46,153 @@ impl From for RowPB { } } +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct RowMetaPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub document_id: String, + + #[pb(index = 3, one_of)] + pub icon: Option, + + #[pb(index = 4, one_of)] + pub cover: Option, +} + +impl std::convert::From<&RowMeta> for RowMetaPB { + fn from(row_meta: &RowMeta) -> Self { + Self { + id: row_meta.row_id.clone(), + document_id: row_meta.document_id.clone(), + icon: row_meta.icon_url.clone(), + cover: row_meta.cover_url.clone(), + } + } +} + +impl std::convert::From for RowMetaPB { + fn from(row_meta: RowMeta) -> Self { + Self { + id: row_meta.row_id, + document_id: row_meta.document_id, + icon: row_meta.icon_url, + cover: row_meta.cover_url, + } + } +} + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct UpdateRowMetaChangesetPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub view_id: String, + + #[pb(index = 3, one_of)] + pub icon_url: Option, + + #[pb(index = 4, one_of)] + pub cover_url: Option, +} + +#[derive(Debug)] +pub struct UpdateRowMetaParams { + pub id: String, + pub view_id: String, + pub icon_url: Option, + pub cover_url: Option, +} + +impl TryInto for UpdateRowMetaChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let row_id = NotEmptyStr::parse(self.id) + .map_err(|_| ErrorCode::RowIdIsEmpty)? + .0; + + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::ViewIdIsInvalid)? + .0; + Ok(UpdateRowMetaParams { + id: row_id, + view_id, + icon_url: self.icon_url, + cover_url: self.cover_url, + }) + } +} + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct UpdateRowPayloadPB { + #[pb(index = 1)] + pub row_id: String, + + #[pb(index = 2, one_of)] + pub insert_document: Option, + + #[pb(index = 3, one_of)] + pub insert_comment: Option, +} + +#[derive(Debug, Default, Clone)] +pub struct UpdateRowParams { + pub row_id: String, + pub insert_comment: Option, +} + +impl TryInto for UpdateRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let row_id = NotEmptyStr::parse(self.row_id) + .map_err(|_| ErrorCode::RowIdIsEmpty)? + .0; + let insert_comment = self + .insert_comment + .map(|comment| comment.try_into()) + .transpose()?; + + Ok(UpdateRowParams { + row_id, + insert_comment, + }) + } +} + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct RowCommentPayloadPB { + #[pb(index = 1)] + pub uid: String, + + #[pb(index = 2)] + pub comment: String, +} + +#[derive(Debug, Default, Clone)] +pub struct RowCommentParams { + pub uid: String, + pub comment: String, +} + +impl TryInto for RowCommentPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let uid = NotEmptyStr::parse(self.uid) + .map_err(|_| ErrorCode::RowIdIsEmpty)? + .0; + let comment = NotEmptyStr::parse(self.comment) + .map_err(|_| ErrorCode::RowIdIsEmpty)? + .0; + + Ok(RowCommentParams { uid, comment }) + } +} + #[derive(Debug, Default, ProtoBuf)] pub struct OptionalRowPB { #[pb(index = 1, one_of)] @@ -66,7 +214,7 @@ impl std::convert::From> for RepeatedRowPB { #[derive(Debug, Clone, Default, ProtoBuf)] pub struct InsertedRowPB { #[pb(index = 1)] - pub row: RowPB, + pub row_meta: RowMetaPB, #[pb(index = 2, one_of)] pub index: Option, @@ -76,9 +224,9 @@ pub struct InsertedRowPB { } impl InsertedRowPB { - pub fn new(row: RowPB) -> Self { + pub fn new(row_meta: RowMetaPB) -> Self { Self { - row, + row_meta, index: None, is_new: false, } @@ -90,26 +238,20 @@ impl InsertedRowPB { } } -impl std::convert::From for InsertedRowPB { - fn from(row: RowPB) -> Self { +impl std::convert::From for InsertedRowPB { + fn from(row_meta: RowMetaPB) -> Self { Self { - row, + row_meta, index: None, is_new: false, } } } -impl std::convert::From<&Row> for InsertedRowPB { - fn from(row: &Row) -> Self { - Self::from(RowPB::from(row)) - } -} - impl From for InsertedRowPB { fn from(data: InsertedRow) -> Self { Self { - row: data.row.into(), + row_meta: data.row_meta.into(), index: data.index, is_new: data.is_new, } @@ -119,18 +261,24 @@ impl From for InsertedRowPB { #[derive(Debug, Clone, Default, ProtoBuf)] pub struct UpdatedRowPB { #[pb(index = 1)] - pub row: RowPB, + pub row_id: String, // Indicates the field ids of the cells that were updated in this row. #[pb(index = 2)] pub field_ids: Vec, + + /// The meta of row was updated if this is Some. + #[pb(index = 3, one_of)] + pub row_meta: Option, } impl From for UpdatedRowPB { fn from(data: UpdatedRow) -> Self { + let row_meta = data.row_meta.map(RowMetaPB::from); Self { - row: data.row.into(), + row_id: data.row_id, field_ids: data.field_ids, + row_meta, } } } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index f6570d0d7d..6730246f1f 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -133,6 +133,34 @@ pub(crate) async fn get_fields_handler( data_result_ok(fields) } +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_primary_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let view_id = data.into_inner().value; + let database_editor = manager.get_database_with_view_id(&view_id).await?; + let mut fields = database_editor + .get_fields(&view_id, None) + .into_iter() + .filter(|field| field.is_primary) + .map(FieldPB::from) + .collect::>(); + + if fields.is_empty() { + // The primary field should not be empty. Because it is created when the database is created. + // If it is empty, it must be a bug. + Err(FlowyError::record_not_found()) + } else { + if fields.len() > 1 { + // The primary field should not be more than one. If it is more than one, + // it must be a bug. + tracing::error!("The primary field is more than one"); + } + data_result_ok(fields.remove(0)) + } +} + #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn update_field_handler( data: AFPluginData, @@ -300,6 +328,29 @@ pub(crate) async fn get_row_handler( data_result_ok(OptionalRowPB { row }) } +pub(crate) async fn get_row_meta_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: RowIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + match database_editor.get_row_meta(¶ms.view_id, ¶ms.row_id) { + None => Err(FlowyError::record_not_found()), + Some(row) => data_result_ok(row), + } +} + +pub(crate) async fn update_row_meta_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let params: UpdateRowMetaParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let row_id = RowId::from(params.id.clone()); + database_editor.update_row_meta(&row_id, params).await; + Ok(()) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn delete_row_handler( data: AFPluginData, @@ -341,7 +392,7 @@ pub(crate) async fn move_row_handler( pub(crate) async fn create_row_handler( data: AFPluginData, manager: AFPluginState>, -) -> DataResult { +) -> DataResult { let params: CreateRowParams = data.into_inner().try_into()?; let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let fields = database_editor.get_fields(¶ms.view_id, None); @@ -362,7 +413,7 @@ pub(crate) async fn create_row_handler( .await? { None => Err(FlowyError::internal().context("Create row fail")), - Some(row) => data_result_ok(RowPB::from(row)), + Some(row) => data_result_ok(RowMetaPB::from(row.meta)), } } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index a39c7ffbe4..eaf644daec 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -22,6 +22,7 @@ pub fn init(database_manager: Arc) -> AFPlugin { .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler) // Field .event(DatabaseEvent::GetFields, get_fields_handler) + .event(DatabaseEvent::GetPrimaryField, get_primary_field_handler) .event(DatabaseEvent::UpdateField, update_field_handler) .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) .event(DatabaseEvent::DeleteField, delete_field_handler) @@ -33,6 +34,8 @@ pub fn init(database_manager: Arc) -> AFPlugin { // Row .event(DatabaseEvent::CreateRow, create_row_handler) .event(DatabaseEvent::GetRow, get_row_handler) + .event(DatabaseEvent::GetRowMeta, get_row_meta_handler) + .event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler) .event(DatabaseEvent::DeleteRow, delete_row_handler) .event(DatabaseEvent::DuplicateRow, duplicate_row_handler) .event(DatabaseEvent::MoveRow, move_row_handler) @@ -172,6 +175,9 @@ pub enum DatabaseEvent { #[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")] CreateTypeOption = 24, + #[event(input = "DatabaseViewIdPB", output = "FieldPB")] + GetPrimaryField = 25, + /// [CreateSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if /// there are no errors. #[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")] @@ -195,7 +201,7 @@ pub enum DatabaseEvent { #[event(input = "RepeatedSelectOptionPayload")] DeleteSelectOption = 33, - #[event(input = "CreateRowPayloadPB", output = "RowPB")] + #[event(input = "CreateRowPayloadPB", output = "RowMetaPB")] CreateRow = 50, /// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables @@ -212,6 +218,12 @@ pub enum DatabaseEvent { #[event(input = "MoveRowPayloadPB")] MoveRow = 54, + #[event(input = "RowIdPB", output = "RowMetaPB")] + GetRowMeta = 55, + + #[event(input = "UpdateRowMetaChangesetPB")] + UpdateRowMeta = 56, + #[event(input = "CellIdPB", output = "CellPB")] GetCell = 70, diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index 3a7e1911c5..f170e14da9 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -31,6 +31,8 @@ pub enum DatabaseNotification { DidReorderRows = 65, /// Trigger after editing the row that hit the sort rule DidReorderSingleRow = 66, + /// Trigger after updating the row meta + DidUpdateRowMeta = 67, /// Trigger when the settings of the database are changed DidUpdateSettings = 70, // Trigger when the layout setting of the database is updated 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 5654271537..72b9e2fab2 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 @@ -14,19 +14,13 @@ use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_task::TaskDispatcher; use lib_infra::future::{to_fut, Fut}; -use crate::entities::{ - CalendarEventPB, CellChangesetNotifyPB, CellPB, ChecklistCellDataPB, DatabaseFieldChangesetPB, - DatabasePB, DatabaseViewSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams, - FieldChangesetParams, FieldIdPB, FieldPB, FieldType, GroupPB, IndexFieldPB, InsertedRowPB, - LayoutSettingParams, NoDateCalendarEventPB, RepeatedFilterPB, RepeatedGroupPB, RepeatedSortPB, - RowPB, RowsChangePB, SelectOptionCellDataPB, SelectOptionPB, UpdateFilterParams, - UpdateSortParams, UpdatedRowPB, -}; +use crate::entities::*; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::cell::{ apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset, }; use crate::services::database::util::database_view_setting_pb_from_view; +use crate::services::database::{RowDetail, UpdatedRow}; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews}; use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; use crate::services::field::{ @@ -376,8 +370,8 @@ impl DatabaseEditor { pub async fn move_row(&self, view_id: &str, from: RowId, to: RowId) { let database = self.database.lock(); - if let (Some(row), Some(from_index), Some(to_index)) = ( - database.get_row(&from), + if let (Some(row_meta), Some(from_index), Some(to_index)) = ( + database.get_row_meta(&from), database.index_of_row(view_id, &from), database.index_of_row(view_id, &to), ) { @@ -387,7 +381,7 @@ impl DatabaseEditor { drop(database); let delete_row_id = from.into_inner(); - let insert_row = InsertedRowPB::from(&row).with_index(to_index as i32); + let insert_row = InsertedRowPB::new(RowMetaPB::from(&row_meta)).with_index(to_index as i32); let changes = RowsChangePB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]); send_notification(view_id, DatabaseNotification::DidUpdateViewRows) @@ -401,7 +395,7 @@ impl DatabaseEditor { view_id: &str, group_id: Option, mut params: CreateRowParams, - ) -> FlowyResult> { + ) -> FlowyResult> { for view in self.database_views.editors().await { view.v_will_create_row(&mut params.cells, &group_id).await; } @@ -409,11 +403,13 @@ impl DatabaseEditor { if let Some((index, row_order)) = result { tracing::trace!("create row: {:?} at {}", row_order, index); let row = self.database.lock().get_row(&row_order.id); - if let Some(row) = row { + let row_meta = self.database.lock().get_row_meta(&row_order.id); + if let (Some(row), Some(meta)) = (row, row_meta) { + let row_detail = RowDetail { row, meta }; for view in self.database_views.editors().await { - view.v_did_create_row(&row, &group_id, index).await; + view.v_did_create_row(&row_detail, &group_id, index).await; } - return Ok(Some(row)); + return Ok(Some(row_detail)); } } @@ -491,16 +487,42 @@ impl DatabaseEditor { Ok(()) } - pub async fn get_rows(&self, view_id: &str) -> FlowyResult>> { + pub async fn get_rows(&self, view_id: &str) -> FlowyResult>> { let view_editor = self.database_views.get_view_editor(view_id).await?; Ok(view_editor.v_get_rows().await) } pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option { if self.database.lock().views.is_row_exist(view_id, row_id) { - return None; - } else { self.database.lock().get_row(row_id) + } else { + None + } + } + + pub fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option { + if self.database.lock().views.is_row_exist(view_id, row_id) { + let row_meta = self.database.lock().get_row_meta(row_id)?; + Some(RowMetaPB { + id: row_id.clone().into_inner(), + document_id: row_meta.document_id, + icon: row_meta.icon_url, + cover: row_meta.cover_url, + }) + } else { + tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); + None + } + } + + pub fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option { + if self.database.lock().views.is_row_exist(view_id, row_id) { + let meta = self.database.lock().get_row_meta(row_id)?; + let row = self.database.lock().get_row(row_id)?; + Some(RowDetail { row, meta }) + } else { + tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); + None } } @@ -514,6 +536,28 @@ impl DatabaseEditor { } } + #[tracing::instrument(level = "trace", skip_all)] + pub async fn update_row_meta(&self, row_id: &RowId, changeset: UpdateRowMetaParams) { + self.database.lock().update_row_meta(row_id, |meta_update| { + meta_update + .insert_cover_if_not_none(changeset.cover_url) + .insert_icon_if_not_none(changeset.icon_url); + }); + + // Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait. + let row_meta = self.database.lock().get_row_meta(row_id); + if let Some(row_meta) = row_meta { + for view in self.database_views.editors().await { + view.v_did_update_row_meta(row_id, &row_meta).await; + } + + // Notifies the client that the row meta has been updated. + send_notification(row_id.as_str(), DatabaseNotification::DidUpdateRowMeta) + .payload(RowMetaPB::from(&row_meta)) + .send(); + } + } + pub async fn get_cell(&self, field_id: &str, row_id: &RowId) -> Option { let database = self.database.lock(); let field = database.fields.get_field(field_id)?; @@ -630,7 +674,7 @@ impl DatabaseEditor { new_cell: Cell, ) -> FlowyResult<()> { // Get the old row before updating the cell. It would be better to get the old cell - let old_row = { self.database.lock().get_row(&row_id) }; + let old_row = { self.get_row_detail(view_id, &row_id) }; // Get all auto updated fields. It will be used to notify the frontend // that the fields have been updated. @@ -642,19 +686,19 @@ impl DatabaseEditor { }); }); - let option_row = self.database.lock().get_row(&row_id); - if let Some(new_row) = option_row { - let updated_row = UpdatedRowPB { - row: RowPB::from(&new_row), - field_ids: vec![field_id.to_string()], - }; - let changes = RowsChangePB::from_update(view_id.to_string(), updated_row); + let option_row = self.get_row_detail(view_id, &row_id); + if let Some(new_row_detail) = option_row { + let updated_row = + UpdatedRow::new(&new_row_detail.row.id).with_field_ids(vec![field_id.to_string()]); + let changes = RowsChangePB::from_update(view_id.to_string(), updated_row.into()); send_notification(view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); for view in self.database_views.editors().await { - view.v_did_update_row(&old_row, &new_row, field_id).await; + view + .v_did_update_row(&old_row, &new_row_detail, field_id) + .await; } } @@ -854,23 +898,23 @@ impl DatabaseEditor { from_row: RowId, to_row: Option, ) -> FlowyResult<()> { - let row = self.database.lock().get_row(&from_row); - match row { + let row_detail = self.get_row_detail(view_id, &from_row); + match row_detail { None => { tracing::warn!( "Move row between group failed, can not find the row:{}", from_row ) }, - Some(row) => { - let mut row_changeset = RowChangeset::new(row.id.clone()); + Some(row_detail) => { + let mut row_changeset = RowChangeset::new(row_detail.row.id.clone()); let view = self.database_views.get_view_editor(view_id).await?; view - .v_move_group_row(&row, &mut row_changeset, to_group, to_row) + .v_move_group_row(&row_detail, &mut row_changeset, to_group, to_row) .await; tracing::trace!("Row data changed: {:?}", row_changeset); - self.database.lock().update_row(&row.id, |row| { + self.database.lock().update_row(&row_detail.row.id, |row| { row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone())); }); @@ -1012,8 +1056,8 @@ impl DatabaseEditor { let rows = rows .into_iter() - .map(|row| RowPB::from(row.as_ref())) - .collect::>(); + .map(|row_detail| RowMetaPB::from(&row_detail.meta)) + .collect::>(); Ok(DatabasePB { id: database_id, fields, @@ -1150,20 +1194,37 @@ impl DatabaseViewData for DatabaseViewDataImpl { to_fut(async move { index }) } - fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>> { + fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>> { let index = self.database.lock().index_of_row(view_id, row_id); let row = self.database.lock().get_row(row_id); + let row_meta = self.database.lock().get_row_meta(row_id); to_fut(async move { - match (index, row) { - (Some(index), Some(row)) => Some((index, Arc::new(row))), + match (index, row, row_meta) { + (Some(index), Some(row), Some(row_meta)) => { + let row_detail = RowDetail { + row, + meta: row_meta, + }; + Some((index, Arc::new(row_detail))) + }, _ => None, } }) } - fn get_rows(&self, view_id: &str) -> Fut>> { - let rows = self.database.lock().get_rows_for_view(view_id); - to_fut(async move { rows.into_iter().map(Arc::new).collect() }) + fn get_rows(&self, view_id: &str) -> Fut>> { + let database = self.database.lock(); + let rows = database.get_rows_for_view(view_id); + let row_details = rows + .into_iter() + .flat_map(|row| { + database + .get_row_meta(&row.id) + .map(|meta| RowDetail { row, meta }) + }) + .collect::>(); + + to_fut(async move { row_details.into_iter().map(Arc::new).collect() }) } fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>> { diff --git a/frontend/rust-lib/flowy-database2/src/services/database/entities.rs b/frontend/rust-lib/flowy-database2/src/services/database/entities.rs index ec3df071a2..b691dc7200 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/entities.rs @@ -1,5 +1,5 @@ -use collab_database::rows::RowId; -use collab_database::views::{DatabaseLayout, RowOrder}; +use collab_database::rows::{Row, RowId, RowMeta}; +use collab_database::views::DatabaseLayout; #[derive(Debug, Clone)] pub enum DatabaseRowEvent { @@ -14,16 +14,48 @@ pub enum DatabaseRowEvent { #[derive(Debug, Clone)] pub struct InsertedRow { - pub row: RowOrder, + pub row_meta: RowMeta, pub index: Option, pub is_new: bool, } #[derive(Debug, Clone)] pub struct UpdatedRow { - pub row: RowOrder, - // represents as the cells that were updated in this row. + pub row_id: String, + + pub height: Option, + + /// Indicates which cells were updated. pub field_ids: Vec, + + /// The meta of row was updated if this is Some. + pub row_meta: Option, +} + +impl UpdatedRow { + pub fn new(row_id: &str) -> Self { + Self { + row_id: row_id.to_string(), + height: None, + field_ids: vec![], + row_meta: None, + } + } + + pub fn with_field_ids(mut self, field_ids: Vec) -> Self { + self.field_ids = field_ids; + self + } + + pub fn with_height(mut self, height: i32) -> Self { + self.height = Some(height); + self + } + + pub fn with_row_meta(mut self, row_meta: RowMeta) -> Self { + self.row_meta = Some(row_meta); + self + } } #[derive(Debug, Clone)] @@ -32,3 +64,9 @@ pub struct CreateDatabaseViewParams { pub view_id: String, pub layout_type: DatabaseLayout, } + +#[derive(Debug, Clone)] +pub struct RowDetail { + pub row: Row, + pub meta: RowMeta, +} 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 f9abd8c11a..429147b499 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 @@ -4,8 +4,8 @@ use std::sync::Arc; use collab_database::database::{gen_database_filter_id, gen_database_sort_id}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowCell, RowId}; -use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, RowOrder}; +use collab_database::rows::{Cells, Row, RowCell, RowId, RowMeta}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use tokio::sync::{broadcast, RwLock}; use flowy_error::{FlowyError, FlowyResult}; @@ -15,12 +15,14 @@ use lib_infra::future::Fut; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams, FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB, - InsertedRowPB, LayoutSettingParams, RowPB, RowsChangePB, SortChangesetNotificationPB, SortPB, + InsertedRowPB, LayoutSettingParams, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::cell::CellCache; -use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow}; +use crate::services::database::{ + database_view_setting_pb_from_view, DatabaseRowEvent, RowDetail, UpdatedRow, +}; use crate::services::database_view::view_filter::make_filter_controller; use crate::services::database_view::view_group::{ get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field, @@ -63,10 +65,10 @@ pub trait DatabaseViewData: Send + Sync + 'static { fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut>; /// Returns the `index` and `RowRevision` with row_id - fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>>; + fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>>; /// Returns all the rows in the view - fn get_rows(&self, view_id: &str) -> Fut>>; + fn get_rows(&self, view_id: &str) -> Fut>>; fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; @@ -198,11 +200,24 @@ impl DatabaseViewEditor { .await; } - pub async fn v_did_create_row(&self, row: &Row, group_id: &Option, index: usize) { + pub async fn v_did_update_row_meta(&self, row_id: &RowId, row_meta: &RowMeta) { + let update_row = UpdatedRow::new(row_id.as_str()).with_row_meta(row_meta.clone()); + let changeset = RowsChangePB::from_update(self.view_id.clone(), update_row.into()); + send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) + .payload(changeset) + .send(); + } + + pub async fn v_did_create_row( + &self, + row_detail: &RowDetail, + group_id: &Option, + index: usize, + ) { // Send the group notification if the current view has groups match group_id.as_ref() { None => { - let row = InsertedRowPB::from(row).with_index(index as i32); + let row = InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)).with_index(index as i32); let changes = RowsChangePB::from_insert(self.view_id.clone(), row); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) @@ -211,13 +226,13 @@ impl DatabaseViewEditor { Some(group_id) => { self .mut_group_controller(|group_controller, _| { - group_controller.did_create_row(row, group_id); + group_controller.did_create_row(row_detail, group_id); Ok(()) }) .await; let inserted_row = InsertedRowPB { - row: RowPB::from(row), + row_meta: RowMetaPB::from(&row_detail.meta), index: Some(index as i32), is_new: true, }; @@ -251,10 +266,15 @@ impl DatabaseViewEditor { /// Notify the view that the row has been updated. If the view has groups, /// send the group notification with [GroupRowsNotificationPB]. Otherwise, /// send the view notification with [RowsChangePB] - pub async fn v_did_update_row(&self, old_row: &Option, row: &Row, field_id: &str) { + pub async fn v_did_update_row( + &self, + old_row: &Option, + row_detail: &RowDetail, + field_id: &str, + ) { let result = self .mut_group_controller(|group_controller, field| { - Ok(group_controller.did_update_group_row(old_row, row, &field)) + Ok(group_controller.did_update_group_row(old_row, row_detail, &field)) }) .await; @@ -283,10 +303,8 @@ impl DatabaseViewEditor { } } } else { - let update_row = UpdatedRow { - row: RowOrder::from(row), - field_ids: vec![field_id.to_string()], - }; + let update_row = + UpdatedRow::new(&row_detail.row.id).with_field_ids(vec![field_id.to_string()]); let changeset = RowsChangePB::from_update(self.view_id.clone(), update_row.into()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changeset) @@ -295,7 +313,7 @@ impl DatabaseViewEditor { // Each row update will trigger a filter and sort operation. We don't want // to block the main thread, so we spawn a new task to do the work. - let row_id = row.id.clone(); + let row_id = row_detail.row.id.clone(); let weak_filter_controller = Arc::downgrade(&self.filter_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller); tokio::spawn(async move { @@ -314,15 +332,20 @@ impl DatabaseViewEditor { }); } - pub async fn v_filter_rows(&self, rows: &mut Vec>) { - self.filter_controller.filter_rows(rows).await + pub async fn v_filter_rows(&self, row_details: &mut Vec>) { + self.filter_controller.filter_rows(row_details).await } - pub async fn v_sort_rows(&self, rows: &mut Vec>) { - self.sort_controller.write().await.sort_rows(rows).await + pub async fn v_sort_rows(&self, row_details: &mut Vec>) { + self + .sort_controller + .write() + .await + .sort_rows(row_details) + .await } - pub async fn v_get_rows(&self) -> Vec> { + pub async fn v_get_rows(&self) -> Vec> { let mut rows = self.delegate.get_rows(&self.view_id).await; self.v_filter_rows(&mut rows).await; self.v_sort_rows(&mut rows).await; @@ -331,7 +354,7 @@ impl DatabaseViewEditor { pub async fn v_move_group_row( &self, - row: &Row, + row_detail: &RowDetail, row_changeset: &mut RowChangeset, to_group_id: &str, to_row_id: Option, @@ -339,7 +362,7 @@ impl DatabaseViewEditor { let result = self .mut_group_controller(|group_controller, field| { let move_row_context = MoveGroupRowContext { - row, + row_detail, row_changeset, field: field.as_ref(), to_group_id, @@ -741,8 +764,9 @@ impl DatabaseViewEditor { .timestamp .unwrap_or_default(); + let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; Some(CalendarEventPB { - row_id: row_id.into_inner(), + row_meta: RowMetaPB::from(&row_detail.meta), date_field_id: date_field.id.clone(), title, timestamp, @@ -792,8 +816,9 @@ impl DatabaseViewEditor { .unwrap_or_default() .into(); + let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; let event = CalendarEventPB { - row_id: row_id.into_inner(), + row_meta: RowMetaPB::from(&row_detail.meta), date_field_id: calendar_setting.field_id.clone(), title, timestamp, diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index 581fceef32..88c65f76ac 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -1,12 +1,16 @@ +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::RowId; + +use lib_infra::future::{to_fut, Fut}; + use crate::services::cell::CellCache; +use crate::services::database::RowDetail; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, }; use crate::services::filter::{Filter, FilterController, FilterDelegate, FilterTaskHandler}; -use collab_database::fields::Field; -use collab_database::rows::{Row, RowId}; -use lib_infra::future::{to_fut, Fut}; -use std::sync::Arc; pub async fn make_filter_controller( view_id: &str, @@ -56,11 +60,11 @@ impl FilterDelegate for DatabaseViewFilterDelegateImpl { self.0.get_fields(view_id, field_ids) } - fn get_rows(&self, view_id: &str) -> Fut>> { + fn get_rows(&self, view_id: &str) -> Fut>> { self.0.get_rows(view_id) } - fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>> { + fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>> { self.0.get_row(view_id, rows_id) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index 7cac41b3a0..b450a99fbc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -1,14 +1,17 @@ +use std::sync::Arc; + +use collab_database::fields::Field; +use tokio::sync::RwLock; + +use lib_infra::future::{to_fut, Fut}; + use crate::services::cell::CellCache; +use crate::services::database::RowDetail; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, }; use crate::services::filter::FilterController; use crate::services::sort::{Sort, SortController, SortDelegate, SortTaskHandler}; -use collab_database::fields::Field; -use collab_database::rows::Row; -use lib_infra::future::{to_fut, Fut}; -use std::sync::Arc; -use tokio::sync::RwLock; pub(crate) async fn make_sort_controller( view_id: &str, @@ -56,14 +59,14 @@ impl SortDelegate for DatabaseViewSortDelegateImpl { to_fut(async move { sort }) } - fn get_rows(&self, view_id: &str) -> Fut>> { + fn get_rows(&self, view_id: &str) -> Fut>> { let view_id = view_id.to_string(); let delegate = self.delegate.clone(); let filter_controller = self.filter_controller.clone(); to_fut(async move { - let mut rows = delegate.get_rows(&view_id).await; - filter_controller.filter_rows(&mut rows).await; - rows + let mut row_details = delegate.get_rows(&view_id).await; + filter_controller.filter_rows(&mut row_details).await; + row_details }) } 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 7766d75ec2..0fdd4651fb 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 @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Row, RowId}; +use collab_database::rows::RowId; use nanoid::nanoid; use tokio::sync::{broadcast, RwLock}; @@ -10,7 +10,7 @@ use flowy_error::FlowyResult; use lib_infra::future::Fut; use crate::services::cell::CellCache; -use crate::services::database::{DatabaseRowEvent, MutexDatabase}; +use crate::services::database::{DatabaseRowEvent, MutexDatabase, RowDetail}; use crate::services::database_view::{DatabaseViewData, DatabaseViewEditor}; use crate::services::group::RowChangeset; @@ -58,15 +58,15 @@ impl DatabaseViews { pub async fn move_group_row( &self, view_id: &str, - row: Arc, + row_detail: Arc, to_group_id: String, to_row_id: Option, recv_row_changeset: impl FnOnce(RowChangeset) -> Fut<()>, ) -> FlowyResult<()> { let view_editor = self.get_view_editor(view_id).await?; - let mut row_changeset = RowChangeset::new(row.id.clone()); + let mut row_changeset = RowChangeset::new(row_detail.row.id.clone()); view_editor - .v_move_group_row(&row, &mut row_changeset, &to_group_id, to_row_id) + .v_move_group_row(&row_detail, &mut row_changeset, &to_group_id, to_row_id) .await; if !row_changeset.is_empty() { diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 5062839ff5..6b60ed784f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -13,8 +13,9 @@ use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use lib_infra::future::Fut; use crate::entities::filter_entities::*; -use crate::entities::{FieldType, InsertedRowPB, RowPB}; +use crate::entities::{FieldType, InsertedRowPB, RowMetaPB}; use crate::services::cell::{AnyTypeCache, CellCache, CellFilterCache}; +use crate::services::database::RowDetail; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::field::*; use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification}; @@ -23,8 +24,8 @@ pub trait FilterDelegate: Send + Sync + 'static { fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>>; fn get_field(&self, field_id: &str) -> Fut>>; fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; - fn get_rows(&self, view_id: &str) -> Fut>>; - fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>>; + fn get_rows(&self, view_id: &str) -> Fut>>; + fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>>; } pub trait FromFilterString { @@ -98,14 +99,14 @@ impl FilterController { self.task_scheduler.write().await.add_task(task); } - pub async fn filter_rows(&self, rows: &mut Vec>) { + pub async fn filter_rows(&self, rows: &mut Vec>) { if self.cell_filter_cache.read().is_empty() { return; } let field_by_field_id = self.get_field_map().await; - rows.iter().for_each(|row| { + rows.iter().for_each(|row_detail| { let _ = filter_row( - row, + &row_detail.row, &self.result_by_row_id, &field_by_field_id, &self.cell_cache, @@ -113,10 +114,10 @@ impl FilterController { ); }); - rows.retain(|row| { + rows.retain(|row_detail| { self .result_by_row_id - .get(&row.id) + .get(&row_detail.row.id) .map(|result| result.is_visible()) .unwrap_or(false) }); @@ -149,22 +150,21 @@ impl FilterController { } async fn filter_row(&self, row_id: RowId) -> FlowyResult<()> { - if let Some((_, row)) = self.delegate.get_row(&self.view_id, &row_id).await { + if let Some((_, row_detail)) = self.delegate.get_row(&self.view_id, &row_id).await { let field_by_field_id = self.get_field_map().await; let mut notification = FilterResultNotification::new(self.view_id.clone()); if let Some((row_id, is_visible)) = filter_row( - &row, + &row_detail.row, &self.result_by_row_id, &field_by_field_id, &self.cell_cache, &self.cell_filter_cache, ) { if is_visible { - if let Some((index, row)) = self.delegate.get_row(&self.view_id, &row_id).await { - let row_pb = RowPB::from(row.as_ref()); + if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { notification .visible_rows - .push(InsertedRowPB::new(row_pb).with_index(index as i32)) + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)).with_index(index as i32)) } } else { notification.invisible_rows.push(row_id); @@ -183,7 +183,7 @@ impl FilterController { let mut visible_rows = vec![]; let mut invisible_rows = vec![]; - for (index, row) in self + for (index, row_detail) in self .delegate .get_rows(&self.view_id) .await @@ -191,15 +191,15 @@ impl FilterController { .enumerate() { if let Some((row_id, is_visible)) = filter_row( - &row, + &row_detail.row, &self.result_by_row_id, &field_by_field_id, &self.cell_cache, &self.cell_filter_cache, ) { if is_visible { - let row_pb = RowPB::from(row.as_ref()); - visible_rows.push(InsertedRowPB::new(row_pb).with_index(index as i32)) + let row_meta = RowMetaPB::from(&row_detail.meta); + visible_rows.push(InsertedRowPB::new(row_meta).with_index(index as i32)) } else { invisible_rows.push(row_id); } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 8759301d60..15401267f0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,12 +1,14 @@ -use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; -use crate::services::cell::DecodedCellData; -use crate::services::group::controller::MoveGroupRowContext; -use crate::services::group::{GroupData, GroupSettingChangeset}; use collab_database::fields::Field; use collab_database::rows::{Cell, Row}; use flowy_error::FlowyResult; +use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; +use crate::services::cell::DecodedCellData; +use crate::services::database::RowDetail; +use crate::services::group::controller::MoveGroupRowContext; +use crate::services::group::{GroupData, GroupSettingChangeset}; + /// Using polymorphism to provides the customs action for different group controller. /// /// For example, the `CheckboxGroupController` implements this trait to provide custom behavior. @@ -28,7 +30,7 @@ pub trait GroupCustomize: Send + Sync { fn create_or_delete_group_when_cell_changed( &mut self, - _row: &Row, + _row_detail: &RowDetail, _old_cell_data: Option<&Self::CellData>, _cell_data: &Self::CellData, ) -> FlowyResult<(Option, Option)> { @@ -40,7 +42,7 @@ pub trait GroupCustomize: Send + Sync { /// fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &Self::CellData, ) -> Vec; @@ -76,7 +78,7 @@ pub trait GroupControllerOperation: Send + Sync { fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)>; /// Separates the rows into different groups - fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()>; + fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()>; /// Remove the group with from_group_id and insert it to the index with to_group_id fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>; @@ -84,8 +86,8 @@ pub trait GroupControllerOperation: Send + Sync { /// Insert/Remove the row to the group if the corresponding cell data is changed fn did_update_group_row( &mut self, - old_row: &Option, - row: &Row, + old_row_detail: &Option, + row_detail: &RowDetail, field: &Field, ) -> FlowyResult; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index e484229931..6bda5d74fb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -9,8 +9,11 @@ use serde::Serialize; use flowy_error::FlowyResult; -use crate::entities::{FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB}; +use crate::entities::{ + FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, +}; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser, DecodedCellData}; +use crate::services::database::RowDetail; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, GroupCustomize, }; @@ -36,7 +39,7 @@ pub trait GroupController: GroupControllerOperation + Send + Sync { fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str); /// Called after the row was created. - fn did_create_row(&mut self, row: &Row, group_id: &str); + fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str); } /// The [GroupsBuilder] trait is used to generate the groups for different [FieldType] @@ -62,7 +65,7 @@ pub struct GeneratedGroupConfig { } pub struct MoveGroupRowContext<'a> { - pub row: &'a Row, + pub row_detail: &'a RowDetail, pub row_changeset: &'a mut RowChangeset, pub field: &'a Field, pub to_group_id: &'a str, @@ -134,7 +137,7 @@ where #[allow(clippy::needless_collect)] fn update_no_status_group( &mut self, - row: &Row, + row_detail: &RowDetail, other_group_changesets: &[GroupRowsNotificationPB], ) -> Option { let no_status_group = self.context.get_mut_no_status_group()?; @@ -155,14 +158,16 @@ where // which means the row should not move to the default group. !other_group_inserted_row .iter() - .any(|inserted_row| &inserted_row.row.id == row_id) + .any(|inserted_row| &inserted_row.row_meta.id == row_id) }) .collect::>(); let mut changeset = GroupRowsNotificationPB::new(no_status_group.id.clone()); if !no_status_group_rows.is_empty() { - changeset.inserted_rows.push(InsertedRowPB::new(row.into())); - no_status_group.add_row(row.clone()); + changeset + .inserted_rows + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + no_status_group.add_row(row_detail.clone()); } // [other_group_delete_rows] contains all the deleted rows except the default group. @@ -180,23 +185,23 @@ where // out from the default_group. !other_group_delete_rows .iter() - .any(|row_id| &inserted_row.row.id == row_id) + .any(|row_id| &inserted_row.row_meta.id == row_id) }) .collect::>(); let mut deleted_row_ids = vec![]; - for row in &no_status_group.rows { - let row_id = row.id.clone().into_inner(); + for row_detail in &no_status_group.rows { + let row_id = row_detail.meta.row_id.clone(); if default_group_deleted_rows .iter() - .any(|deleted_row| deleted_row.row.id == row_id) + .any(|deleted_row| deleted_row.row_meta.id == row_id) { deleted_row_ids.push(row_id); } } no_status_group .rows - .retain(|row| !deleted_row_ids.contains(&row.id)); + .retain(|row_detail| !deleted_row_ids.contains(&row_detail.meta.row_id)); changeset.deleted_rows.extend(deleted_row_ids); Some(changeset) } @@ -225,9 +230,9 @@ where } #[tracing::instrument(level = "trace", skip_all, fields(row_count=%rows.len(), group_result))] - fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()> { - for row in rows { - let cell = match row.cells.get(&self.grouping_field_id) { + fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()> { + for row_detail in rows { + let cell = match row_detail.row.cells.get(&self.grouping_field_id) { None => self.placeholder_cell(), Some(cell) => Some(cell.clone()), }; @@ -239,7 +244,7 @@ where for group in self.context.groups() { if self.can_group(&group.filter_content, &cell_data) { grouped_rows.push(GroupedRow { - row: (*row).clone(), + row_detail: (*row_detail).clone(), group_id: group.id.clone(), }); } @@ -248,7 +253,7 @@ where if !grouped_rows.is_empty() { for group_row in grouped_rows { if let Some(group) = self.context.get_mut_group(&group_row.group_id) { - group.add_row(group_row.row); + group.add_row(group_row.row_detail); } } continue; @@ -257,7 +262,7 @@ where match self.context.get_mut_no_status_group() { None => {}, - Some(no_status_group) => no_status_group.add_row((*row).clone()), + Some(no_status_group) => no_status_group.add_row((*row_detail).clone()), } } @@ -271,8 +276,8 @@ where fn did_update_group_row( &mut self, - old_row: &Option, - row: &Row, + old_row_detail: &Option, + row_detail: &RowDetail, field: &Field, ) -> FlowyResult { // let cell_data = row_rev.cells.get(&self.field_id).and_then(|cell_rev| { @@ -285,18 +290,21 @@ where row_changesets: vec![], }; - if let Some(cell_data) = get_cell_data_from_row::

(Some(row), field) { - let old_row = old_row.as_ref(); - let old_cell_data = get_cell_data_from_row::

(old_row, field); - if let Ok((insert, delete)) = - self.create_or_delete_group_when_cell_changed(row, old_cell_data.as_ref(), &cell_data) - { + if let Some(cell_data) = get_cell_data_from_row::

(Some(&row_detail.row), field) { + let _old_row = old_row_detail.as_ref(); + let old_cell_data = + get_cell_data_from_row::

(old_row_detail.as_ref().map(|detail| &detail.row), field); + if let Ok((insert, delete)) = self.create_or_delete_group_when_cell_changed( + row_detail, + old_cell_data.as_ref(), + &cell_data, + ) { result.inserted_group = insert; result.deleted_group = delete; } - let mut changesets = self.add_or_remove_row_when_cell_changed(row, &cell_data); - if let Some(changeset) = self.update_no_status_group(row, &changesets) { + let mut changesets = self.add_or_remove_row_when_cell_changed(row_detail, &cell_data); + if let Some(changeset) = self.update_no_status_group(row_detail, &changesets) { if !changeset.is_empty() { changesets.push(changeset); } @@ -350,7 +358,7 @@ where deleted_group: None, row_changesets: vec![], }; - let cell = match context.row.cells.get(&self.grouping_field_id) { + let cell = match context.row_detail.row.cells.get(&self.grouping_field_id) { Some(cell) => Some(cell.clone()), None => self.placeholder_cell(), }; @@ -358,7 +366,7 @@ where if let Some(cell) = cell { let cell_bytes = get_cell_protobuf(&cell, context.field, None); let cell_data = cell_bytes.parser::

()?; - result.deleted_group = self.delete_group_when_move_row(context.row, &cell_data); + result.deleted_group = self.delete_group_when_move_row(&context.row_detail.row, &cell_data); result.row_changesets = self.move_row(&cell_data, context); } else { tracing::warn!("Unexpected moving group row, changes should not be empty"); @@ -381,7 +389,7 @@ where } struct GroupedRow { - row: Row, + row_detail: RowDetail, group_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index 7c8ba00c3d..2b67c53f07 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowPB}; +use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; use crate::services::cell::insert_checkbox_cell; +use crate::services::database::RowDetail; use crate::services::field::{ CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOption, CHECK, UNCHECK, }; @@ -49,25 +51,27 @@ impl GroupCustomize for CheckboxGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &Self::CellData, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); - let is_not_contained = !group.contains_row(&row.id); + let is_not_contained = !group.contains_row(&row_detail.row.id); if group.id == CHECK { if cell_data.is_uncheck() { // Remove the row if the group.id is CHECK but the cell_data is UNCHECK - changeset.deleted_rows.push(row.id.clone().into_inner()); - group.remove_row(&row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + group.remove_row(&row_detail.row.id); } else { // Add the row to the group if the group didn't contain the row if is_not_contained { changeset .inserted_rows - .push(InsertedRowPB::new(RowPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + group.add_row(row_detail.clone()); } } } @@ -75,15 +79,17 @@ impl GroupCustomize for CheckboxGroupController { if group.id == UNCHECK { if cell_data.is_check() { // Remove the row if the group.id is UNCHECK but the cell_data is CHECK - changeset.deleted_rows.push(row.id.clone().into_inner()); - group.remove_row(&row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + group.remove_row(&row_detail.row.id); } else { // Add the row to the group if the group didn't contain the row if is_not_contained { changeset .inserted_rows - .push(InsertedRowPB::new(RowPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + group.add_row(row_detail.clone()); } } } @@ -142,9 +148,9 @@ impl GroupController for CheckboxGroupController { } } - fn did_create_row(&mut self, row: &Row, group_id: &str) { + fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row.clone()) + group.add_row(row_detail.clone()) } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 61e505b824..dde8909047 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -6,6 +6,7 @@ use collab_database::rows::{Cells, Row}; use flowy_error::FlowyResult; use crate::entities::GroupChangesPB; +use crate::services::database::RowDetail; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, }; @@ -52,7 +53,7 @@ impl GroupControllerOperation for DefaultGroupController { Some((0, self.group.clone())) } - fn fill_groups(&mut self, rows: &[&Row], _field: &Field) -> FlowyResult<()> { + fn fill_groups(&mut self, rows: &[&RowDetail], _field: &Field) -> FlowyResult<()> { rows.iter().for_each(|row| { self.group.add_row((*row).clone()); }); @@ -65,8 +66,8 @@ impl GroupControllerOperation for DefaultGroupController { fn did_update_group_row( &mut self, - _old_row: &Option, - _row: &Row, + _old_row_detail: &Option, + _row_detail: &RowDetail, _field: &Field, ) -> FlowyResult { Ok(DidUpdateGroupRowResult { @@ -116,5 +117,5 @@ impl GroupController for DefaultGroupController { fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} - fn did_create_row(&mut self, _row: &Row, _group_id: &str) {} + fn did_create_row(&mut self, _row_detail: &RowDetail, _group_id: &str) {} } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 48f1c6f138..4418a83bf0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,5 +1,12 @@ +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use serde::{Deserialize, Serialize}; + use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; use crate::services::cell::insert_select_option_cell; +use crate::services::database::RowDetail; use crate::services::field::{MultiSelectTypeOption, SelectOptionCellDataParser}; use crate::services::group::action::GroupCustomize; use crate::services::group::controller::{ @@ -9,11 +16,6 @@ use crate::services::group::{ add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group, move_group_row, remove_select_option_row, GeneratedGroups, GroupContext, }; -use collab_database::fields::Field; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; #[derive(Default, Serialize, Deserialize)] pub struct MultiSelectGroupConfiguration { @@ -49,12 +51,12 @@ impl GroupCustomize for MultiSelectGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &Self::CellData, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { - if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row) { + if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row_detail) { changesets.push(changeset); } }); @@ -99,9 +101,9 @@ impl GroupController for MultiSelectGroupController { } } - fn did_create_row(&mut self, row: &Row, group_id: &str) { + fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row.clone()) + group.add_row(row_detail.clone()) } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index ffe3d678b3..ba278e092a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,11 +1,14 @@ -use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; -use crate::services::cell::insert_select_option_cell; -use crate::services::field::{SelectOptionCellDataParser, SingleSelectTypeOption}; -use crate::services::group::action::GroupCustomize; -use collab_database::fields::Field; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; use std::sync::Arc; +use collab_database::fields::Field; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use serde::{Deserialize, Serialize}; + +use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; +use crate::services::cell::insert_select_option_cell; +use crate::services::database::RowDetail; +use crate::services::field::{SelectOptionCellDataParser, SingleSelectTypeOption}; +use crate::services::group::action::GroupCustomize; use crate::services::group::controller::{ BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, }; @@ -13,8 +16,6 @@ use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::GroupData; use crate::services::group::{make_no_status_group, GeneratedGroups, GroupContext}; -use serde::{Deserialize, Serialize}; - #[derive(Default, Serialize, Deserialize)] pub struct SingleSelectGroupConfiguration { pub hide_empty: bool, @@ -49,12 +50,12 @@ impl GroupCustomize for SingleSelectGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &Self::CellData, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { - if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row) { + if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row_detail) { changesets.push(changeset); } }); @@ -99,9 +100,9 @@ impl GroupController for SingleSelectGroupController { }, } } - fn did_create_row(&mut self, row: &Row, group_id: &str) { + fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row.clone()) + group.add_row(row_detail.clone()) } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index c8a1e53148..2cc8249566 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -2,9 +2,10 @@ use collab_database::fields::Field; use collab_database::rows::{Cell, Row}; use crate::entities::{ - FieldType, GroupRowsNotificationPB, InsertedRowPB, RowPB, SelectOptionCellDataPB, + FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, SelectOptionCellDataPB, }; use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell, insert_url_cell}; +use crate::services::database::RowDetail; use crate::services::field::{SelectOption, CHECK}; use crate::services::group::controller::MoveGroupRowContext; use crate::services::group::{GeneratedGroupConfig, Group, GroupData}; @@ -12,26 +13,30 @@ use crate::services::group::{GeneratedGroupConfig, Group, GroupData}; pub fn add_or_remove_select_option_row( group: &mut GroupData, cell_data: &SelectOptionCellDataPB, - row: &Row, + row_detail: &RowDetail, ) -> Option { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); if cell_data.select_options.is_empty() { - if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } } else { cell_data.select_options.iter().for_each(|option| { if option.id == group.id { - if !group.contains_row(&row.id) { + if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + group.add_row(row_detail.clone()); } - } else if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + } else if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } }); } @@ -69,14 +74,14 @@ pub fn move_group_row( ) -> Option { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); let MoveGroupRowContext { - row, + row_detail, row_changeset, field, to_group_id, to_row_id, } = context; - let from_index = group.index_of_row(&row.id); + let from_index = group.index_of_row(&row_detail.row.id); let to_index = match to_row_id { None => None, Some(to_row_id) => group.index_of_row(to_row_id), @@ -84,28 +89,40 @@ pub fn move_group_row( // Remove the row in which group contains it if let Some(from_index) = &from_index { - changeset.deleted_rows.push(row.id.clone().into_inner()); - tracing::debug!("Group:{} remove {} at {}", group.id, row.id, from_index); - group.remove_row(&row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + tracing::debug!( + "Group:{} remove {} at {}", + group.id, + row_detail.row.id, + from_index + ); + group.remove_row(&row_detail.row.id); } if group.id == *to_group_id { - let mut inserted_row = InsertedRowPB::new(RowPB::from(*row)); + let mut inserted_row = InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)); match to_index { None => { changeset.inserted_rows.push(inserted_row); - tracing::debug!("Group:{} append row:{}", group.id, row.id); - group.add_row(row.clone()); + tracing::debug!("Group:{} append row:{}", group.id, row_detail.row.id); + group.add_row(row_detail.clone()); }, Some(to_index) => { if to_index < group.number_of_row() { - tracing::debug!("Group:{} insert {} at {} ", group.id, row.id, to_index); + tracing::debug!( + "Group:{} insert {} at {} ", + group.id, + row_detail.row.id, + to_index + ); inserted_row.index = Some(to_index as i32); - group.insert_row(to_index, (*row).clone()); + group.insert_row(to_index, (*row_detail).clone()); } else { tracing::warn!("Move to index: {} is out of bounds", to_index); - tracing::debug!("Group:{} append row:{}", group.id, row.id); - group.add_row((*row).clone()); + tracing::debug!("Group:{} append row:{}", group.id, row_detail.row.id); + group.add_row((*row_detail).clone()); } changeset.inserted_rows.push(inserted_row); }, @@ -119,7 +136,7 @@ pub fn move_group_row( if let Some(cell) = cell { tracing::debug!( "Update content of the cell in the row:{} to group:{}", - row.id, + row_detail.row.id, group.id ); row_changeset diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index 513ceade0c..b86683c057 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -1,14 +1,17 @@ +use std::sync::Arc; + use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; use serde::{Deserialize, Serialize}; -use std::sync::Arc; use flowy_error::FlowyResult; use crate::entities::{ - FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowPB, URLCellDataPB, + FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, + URLCellDataPB, }; use crate::services::cell::insert_url_cell; +use crate::services::database::RowDetail; use crate::services::field::{URLCellData, URLCellDataParser, URLTypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupContext; @@ -46,7 +49,7 @@ impl GroupCustomize for URLGroupController { fn create_or_delete_group_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, _old_cell_data: Option<&Self::CellData>, _cell_data: &Self::CellData, ) -> FlowyResult<(Option, Option)> { @@ -56,7 +59,7 @@ impl GroupCustomize for URLGroupController { let cell_data: URLCellData = _cell_data.clone().into(); let group = make_group_from_url_cell(&cell_data); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowPB::from(row)); + new_group.group.rows.push(RowMetaPB::from(&row_detail.meta)); inserted_group = Some(new_group); } @@ -87,22 +90,24 @@ impl GroupCustomize for URLGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &Self::CellData, ) -> Vec { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); if group.id == cell_data.content { - if !group.contains_row(&row.id) { + if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + group.add_row(row_detail.clone()); } - } else if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + } else if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } if !changeset.is_empty() { @@ -175,9 +180,9 @@ impl GroupController for URLGroupController { } } - fn did_create_row(&mut self, row: &Row, group_id: &str) { + fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row.clone()) + group.add_row(row_detail.clone()) } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index 31f99e3b88..b88b084aa6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -1,10 +1,12 @@ use anyhow::bail; use collab::core::any_map::AnyMapExtension; use collab_database::database::gen_database_group_id; -use collab_database::rows::{Row, RowId}; +use collab_database::rows::RowId; use collab_database::views::{GroupMap, GroupMapBuilder, GroupSettingBuilder, GroupSettingMap}; use serde::{Deserialize, Serialize}; +use crate::services::database::RowDetail; + #[derive(Debug, Clone, Default)] pub struct GroupSetting { pub id: String, @@ -133,7 +135,7 @@ pub struct GroupData { pub name: String, pub is_default: bool, pub is_visible: bool, - pub(crate) rows: Vec, + pub(crate) rows: Vec, /// [filter_content] is used to determine which group the cell belongs to. pub filter_content: String, @@ -154,11 +156,18 @@ impl GroupData { } pub fn contains_row(&self, row_id: &RowId) -> bool { - self.rows.iter().any(|row| &row.id == row_id) + self + .rows + .iter() + .any(|row_detail| &row_detail.row.id == row_id) } pub fn remove_row(&mut self, row_id: &RowId) { - match self.rows.iter().position(|row| &row.id == row_id) { + match self + .rows + .iter() + .position(|row_detail| &row_detail.row.id == row_id) + { None => {}, Some(pos) => { self.rows.remove(pos); @@ -166,18 +175,18 @@ impl GroupData { } } - pub fn add_row(&mut self, row: Row) { - match self.rows.iter().find(|r| r.id == row.id) { + pub fn add_row(&mut self, row_detail: RowDetail) { + match self.rows.iter().find(|r| r.row.id == row_detail.row.id) { None => { - self.rows.push(row); + self.rows.push(row_detail); }, Some(_) => {}, } } - pub fn insert_row(&mut self, index: usize, row: Row) { + pub fn insert_row(&mut self, index: usize, row_detail: RowDetail) { if index < self.rows.len() { - self.rows.insert(index, row); + self.rows.insert(index, row_detail); } else { tracing::error!( "Insert row index:{} beyond the bounds:{},", @@ -188,7 +197,10 @@ impl GroupData { } pub fn index_of_row(&self, row_id: &RowId) -> Option { - self.rows.iter().position(|row| &row.id == row_id) + self + .rows + .iter() + .position(|row_detail| &row_detail.row.id == row_id) } pub fn number_of_row(&self) -> usize { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index 428a72fa11..688e421e4d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -1,4 +1,12 @@ +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::views::DatabaseLayout; + +use flowy_error::FlowyResult; + use crate::entities::FieldType; +use crate::services::database::RowDetail; use crate::services::group::configuration::GroupSettingReader; use crate::services::group::controller::GroupController; use crate::services::group::{ @@ -6,12 +14,6 @@ use crate::services::group::{ GroupSettingWriter, MultiSelectGroupController, MultiSelectOptionGroupContext, SingleSelectGroupController, SingleSelectOptionGroupContext, URLGroupContext, URLGroupController, }; -use collab_database::fields::Field; -use collab_database::rows::Row; -use collab_database::views::DatabaseLayout; - -use flowy_error::FlowyResult; -use std::sync::Arc; /// Returns a group controller. /// @@ -33,7 +35,7 @@ use std::sync::Arc; pub async fn make_group_controller( view_id: String, grouping_field: Arc, - rows: Vec>, + row_details: Vec>, setting_reader: R, setting_writer: W, ) -> FlowyResult> @@ -99,7 +101,10 @@ where } // Separates the rows into different groups - let rows = rows.iter().map(|row| row.as_ref()).collect::>(); + let rows = row_details + .iter() + .map(|row| row.as_ref()) + .collect::>(); group_controller.fill_groups(rows.as_slice(), &grouping_field)?; Ok(group_controller) } diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index 15229eb81f..c2c85eec86 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -16,6 +16,7 @@ use lib_infra::future::Fut; use crate::entities::FieldType; use crate::entities::SortChangesetNotificationPB; use crate::services::cell::CellCache; +use crate::services::database::RowDetail; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::field::{default_order, TypeOptionCellExt}; use crate::services::sort::{ @@ -25,7 +26,7 @@ use crate::services::sort::{ pub trait SortDelegate: Send + Sync { fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>>; /// Returns all the rows after applying grid's filter - fn get_rows(&self, view_id: &str) -> Fut>>; + fn get_rows(&self, view_id: &str) -> Fut>>; fn get_field(&self, field_id: &str) -> Fut>>; fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; } @@ -90,13 +91,13 @@ impl SortController { // #[tracing::instrument(name = "process_sort_task", level = "trace", skip_all, err)] pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> { let event_type = SortEvent::from_str(predicate).unwrap(); - let mut rows = self.delegate.get_rows(&self.view_id).await; + let mut row_details = self.delegate.get_rows(&self.view_id).await; match event_type { SortEvent::SortDidChanged | SortEvent::DeleteAllSorts => { - self.sort_rows(&mut rows).await; - let row_orders = rows + self.sort_rows(&mut row_details).await; + let row_orders = row_details .iter() - .map(|row| row.id.to_string()) + .map(|row_detail| row_detail.row.id.to_string()) .collect::>(); let notification = ReorderAllRowsResult { @@ -112,7 +113,7 @@ impl SortController { }, SortEvent::RowDidChanged(row_id) => { let old_row_index = self.row_index_cache.get(&row_id).cloned(); - self.sort_rows(&mut rows).await; + self.sort_rows(&mut row_details).await; let new_row_index = self.row_index_cache.get(&row_id).cloned(); match (old_row_index, new_row_index) { (Some(old_row_index), Some(new_row_index)) => { @@ -150,17 +151,20 @@ impl SortController { self.task_scheduler.write().await.add_task(task); } - pub async fn sort_rows(&mut self, rows: &mut Vec>) { + pub async fn sort_rows(&mut self, rows: &mut Vec>) { if self.sorts.is_empty() { return; } - let field_revs = self.delegate.get_fields(&self.view_id, None).await; + let fields = self.delegate.get_fields(&self.view_id, None).await; for sort in self.sorts.iter() { - rows.par_sort_by(|left, right| cmp_row(left, right, sort, &field_revs, &self.cell_cache)); + rows + .par_sort_by(|left, right| cmp_row(&left.row, &right.row, sort, &fields, &self.cell_cache)); } - rows.iter().enumerate().for_each(|(index, row)| { - self.row_index_cache.insert(row.id.clone(), index); + rows.iter().enumerate().for_each(|(index, row_detail)| { + self + .row_index_cache + .insert(row_detail.row.id.clone(), index); }); } @@ -231,10 +235,10 @@ impl SortController { } fn cmp_row( - left: &Arc, - right: &Arc, + left: &Row, + right: &Row, sort: &Arc, - field_revs: &[Arc], + fields: &[Arc], cell_data_cache: &CellCache, ) -> Ordering { let order = match ( @@ -243,7 +247,7 @@ fn cmp_row( ) { (Some(left_cell), Some(right_cell)) => { let field_type = sort.field_type.clone(); - match field_revs + match fields .iter() .find(|field_rev| field_rev.id == sort.field_id) { diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs index 6892b07b5c..648de5edc7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs @@ -11,17 +11,17 @@ use crate::database::block_test::script::RowScript::*; #[tokio::test] async fn created_at_field_test() { let mut test = DatabaseRowTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); test .run_scripts(vec![CreateEmptyRow, AssertRowCount(row_count + 1)]) .await; // Get created time of the new row. - let row = test.get_rows().await.last().cloned().unwrap(); + let row_detail = test.get_rows().await.last().cloned().unwrap(); let updated_at_field = test.get_first_field(FieldType::CreatedTime); let cell = test .editor - .get_cell(&updated_at_field.id, &row.id) + .get_cell(&updated_at_field.id, &row_detail.row.id) .await .unwrap(); let created_at_timestamp = DateCellData::from(&cell).timestamp.unwrap(); @@ -34,11 +34,11 @@ async fn created_at_field_test() { #[tokio::test] async fn update_at_field_test() { let mut test = DatabaseRowTest::new().await; - let row = test.get_rows().await.remove(0); + let row_detail = test.get_rows().await.remove(0); let last_edit_field = test.get_first_field(FieldType::LastEditedTime); let cell = test .editor - .get_cell(&last_edit_field.id, &row.id) + .get_cell(&last_edit_field.id, &row_detail.row.id) .await .unwrap(); let old_updated_at = DateCellData::from(&cell).timestamp.unwrap(); @@ -46,17 +46,17 @@ async fn update_at_field_test() { tokio::time::sleep(Duration::from_millis(1000)).await; test .run_script(UpdateTextCell { - row_id: row.id.clone(), + row_id: row_detail.row.id.clone(), content: "test".to_string(), }) .await; // Get the updated time of the row. - let row = test.get_rows().await.remove(0); + let row_detail = test.get_rows().await.remove(0); let last_edit_field = test.get_first_field(FieldType::LastEditedTime); let cell = test .editor - .get_cell(&last_edit_field.id, &row.id) + .get_cell(&last_edit_field.id, &row_detail.row.id) .await .unwrap(); let new_updated_at = DateCellData::from(&cell).timestamp.unwrap(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs index 50019f7fa5..abe22c353f 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs @@ -1,9 +1,10 @@ -use crate::database::database_editor::DatabaseEditorTest; use collab_database::database::gen_row_id; use collab_database::rows::RowId; use lib_infra::util::timestamp; +use crate::database::database_editor::DatabaseEditorTest; + pub enum RowScript { CreateEmptyRow, UpdateTextCell { row_id: RowId, content: String }, @@ -34,7 +35,7 @@ impl DatabaseRowTest { timestamp: timestamp(), ..Default::default() }; - let row_order = self + let row_detail = self .editor .create_row(&self.view_id, None, params) .await @@ -42,14 +43,14 @@ impl DatabaseRowTest { .unwrap(); self .row_by_row_id - .insert(row_order.id.to_string(), row_order.into()); - self.rows = self.get_rows().await; + .insert(row_detail.row.id.to_string(), row_detail.meta.into()); + self.row_details = self.get_rows().await; }, RowScript::UpdateTextCell { row_id, content } => { self.update_text_cell(row_id, &content).await.unwrap(); }, RowScript::AssertRowCount(expected_row_count) => { - assert_eq!(expected_row_count, self.rows.len()); + assert_eq!(expected_row_count, self.row_details.len()); }, } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs index f5028e265c..62527d6acc 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs @@ -16,10 +16,10 @@ use crate::database::field_test::util::make_date_cell_string; async fn grid_cell_update() { let mut test = DatabaseCellTest::new().await; let fields = test.get_fields(); - let rows = &test.rows; + let rows = &test.row_details; let mut scripts = vec![]; - for (_, row) in rows.iter().enumerate() { + for (_, row_detail) in rows.iter().enumerate() { for field in &fields { let field_type = FieldType::from(field.field_type); let cell_changeset = match field_type { @@ -54,7 +54,7 @@ async fn grid_cell_update() { scripts.push(UpdateCell { changeset: CellChangesetPB { view_id: test.view_id.clone(), - row_id: row.id.clone().into(), + row_id: row_detail.row.id.clone().into(), field_id: field.id.clone(), cell_changeset, }, @@ -125,7 +125,7 @@ async fn update_updated_at_field_on_other_cell_update() { .run_script(UpdateCell { changeset: CellChangesetPB { view_id: test.view_id.clone(), - row_id: test.rows[0].id.to_string(), + row_id: test.row_details[0].row.id.to_string(), field_id: text_field.id.clone(), cell_changeset: "change".to_string(), }, diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 8e046187db..eb549c589d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -3,12 +3,12 @@ use std::sync::Arc; use collab_database::database::{gen_database_view_id, timestamp}; use collab_database::fields::Field; -use collab_database::rows::{CreateRowParams, Row, RowId}; +use collab_database::rows::{CreateRowParams, RowId}; use strum::EnumCount; -use flowy_database2::entities::{FieldType, FilterPB, RowPB, SelectOptionPB}; +use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB, SelectOptionPB}; use flowy_database2::services::cell::{CellBuilder, ToCellChangeset}; -use flowy_database2::services::database::DatabaseEditor; +use flowy_database2::services::database::{DatabaseEditor, RowDetail}; use flowy_database2::services::field::checklist_type_option::{ ChecklistCellChangeset, ChecklistTypeOption, }; @@ -31,9 +31,9 @@ pub struct DatabaseEditorTest { pub view_id: String, pub editor: Arc, pub fields: Vec>, - pub rows: Vec>, + pub row_details: Vec>, pub field_count: usize, - pub row_by_row_id: HashMap, + pub row_by_row_id: HashMap, } impl DatabaseEditorTest { @@ -99,7 +99,7 @@ impl DatabaseEditorTest { view_id, editor, fields, - rows, + row_details: rows, field_count: FieldType::COUNT, row_by_row_id: HashMap::default(), } @@ -109,7 +109,7 @@ impl DatabaseEditorTest { self.editor.get_all_filters(&self.view_id).await.items } - pub async fn get_rows(&self) -> Vec> { + pub async fn get_rows(&self) -> Vec> { self.editor.get_rows(&self.view_id).await.unwrap() } diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs index 97a5bfd272..6898fcd178 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs @@ -128,9 +128,9 @@ impl DatabaseFieldTest { let field_type = FieldType::from(field.field_type); let rows = self.editor.get_rows(&self.view_id()).await.unwrap(); - let row = rows.get(row_index).unwrap(); + let row_detail = rows.get(row_index).unwrap(); - let cell = row.cells.get(&field_id).unwrap().clone(); + let cell = row_detail.row.cells.get(&field_id).unwrap().clone(); let content = stringify_cell_data(&cell, &from_field_type, &field_type, &field); assert_eq!(content, expected_content); }, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs index bb8d193f54..38fa8ed254 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs @@ -1,11 +1,12 @@ +use flowy_database2::entities::CheckboxFilterConditionPB; + use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; -use flowy_database2::entities::CheckboxFilterConditionPB; #[tokio::test] async fn grid_filter_checkbox_is_check_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); // The initial number of unchecked is 3 // The initial number of checked is 2 let scripts = vec![CreateCheckboxFilter { @@ -22,7 +23,7 @@ async fn grid_filter_checkbox_is_check_test() { async fn grid_filter_checkbox_is_uncheck_test() { let mut test = DatabaseFilterTest::new().await; let expected = 3; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let scripts = vec![ CreateCheckboxFilter { condition: CheckboxFilterConditionPB::IsUnChecked, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs index 42a6d33abc..0c58c24d53 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -1,15 +1,16 @@ +use flowy_database2::entities::ChecklistFilterConditionPB; + use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; -use flowy_database2::entities::ChecklistFilterConditionPB; #[tokio::test] async fn grid_filter_checklist_is_incomplete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 5; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let scripts = vec![ UpdateChecklistCell { - row_id: test.rows[0].id.clone(), + row_id: test.row_details[0].row.id.clone(), f: Box::new(|options| options.into_iter().map(|option| option.id).collect()), }, CreateChecklistFilter { @@ -28,10 +29,10 @@ async fn grid_filter_checklist_is_incomplete_test() { async fn grid_filter_checklist_is_complete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 1; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let scripts = vec![ UpdateChecklistCell { - row_id: test.rows[0].id.clone(), + row_id: test.row_details[0].row.id.clone(), f: Box::new(|options| options.into_iter().map(|option| option.id).collect()), }, CreateChecklistFilter { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs index 0c73464bb1..86caf2d8fa 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs @@ -1,11 +1,12 @@ +use flowy_database2::entities::DateFilterConditionPB; + use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; -use flowy_database2::entities::DateFilterConditionPB; #[tokio::test] async fn grid_filter_date_is_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ CreateDateFilter { @@ -26,7 +27,7 @@ async fn grid_filter_date_is_test() { #[tokio::test] async fn grid_filter_date_after_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ CreateDateFilter { @@ -47,7 +48,7 @@ async fn grid_filter_date_after_test() { #[tokio::test] async fn grid_filter_date_on_or_after_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ CreateDateFilter { @@ -68,7 +69,7 @@ async fn grid_filter_date_on_or_after_test() { #[tokio::test] async fn grid_filter_date_on_or_before_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 4; let scripts = vec![ CreateDateFilter { @@ -89,7 +90,7 @@ async fn grid_filter_date_on_or_before_test() { #[tokio::test] async fn grid_filter_date_within_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 5; let scripts = vec![ CreateDateFilter { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs index fb31799039..5597062dc0 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs @@ -1,11 +1,12 @@ +use flowy_database2::entities::NumberFilterConditionPB; + use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; -use flowy_database2::entities::NumberFilterConditionPB; #[tokio::test] async fn grid_filter_number_is_equal_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; let scripts = vec![ CreateNumberFilter { @@ -24,7 +25,7 @@ async fn grid_filter_number_is_equal_test() { #[tokio::test] async fn grid_filter_number_is_less_than_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 2; let scripts = vec![ CreateNumberFilter { @@ -44,7 +45,7 @@ async fn grid_filter_number_is_less_than_test() { #[should_panic] async fn grid_filter_number_is_less_than_test2() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 2; let scripts = vec![ CreateNumberFilter { @@ -63,7 +64,7 @@ async fn grid_filter_number_is_less_than_test2() { #[tokio::test] async fn grid_filter_number_is_less_than_or_equal_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ CreateNumberFilter { @@ -82,7 +83,7 @@ async fn grid_filter_number_is_less_than_or_equal_test() { #[tokio::test] async fn grid_filter_number_is_empty_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; let scripts = vec![ CreateNumberFilter { @@ -101,7 +102,7 @@ async fn grid_filter_number_is_empty_test() { #[tokio::test] async fn grid_filter_number_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 5; let scripts = vec![ CreateNumberFilter { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index 51765eab51..80ca12f72e 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -10,7 +10,7 @@ use collab_database::rows::{Row, RowId}; use futures::TryFutureExt; use tokio::sync::broadcast::Receiver; -use flowy_database2::entities::{UpdateFilterParams, UpdateFilterPayloadPB, CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterParams, FieldType, FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, TextFilterConditionPB, TextFilterPB, SelectOptionPB}; +use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterParams, FieldType, FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, SelectOptionPB, TextFilterConditionPB, TextFilterPB, UpdateFilterParams, UpdateFilterPayloadPB}; use flowy_database2::services::database_view::DatabaseViewChanged; use flowy_database2::services::field::SelectOption; use flowy_database2::services::filter::FilterType; diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs index aeb9fcbe33..7f59f30a93 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs @@ -1,6 +1,7 @@ +use flowy_database2::entities::{FieldType, SelectOptionConditionPB}; + use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; -use flowy_database2::entities::{FieldType, SelectOptionConditionPB}; #[tokio::test] async fn grid_filter_multi_select_is_empty_test() { @@ -62,7 +63,7 @@ async fn grid_filter_multi_select_is_test2() { async fn grid_filter_single_select_is_empty_test() { let mut test = DatabaseFilterTest::new().await; let expected = 2; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let scripts = vec![ CreateSingleSelectFilter { condition: SelectOptionConditionPB::OptionIsEmpty, @@ -83,7 +84,7 @@ async fn grid_filter_single_select_is_test() { let field = test.get_first_field(FieldType::SingleSelect); let mut options = test.get_single_select_type_option(&field.id).options; let expected = 2; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let scripts = vec![ CreateSingleSelectFilter { condition: SelectOptionConditionPB::OptionIs, @@ -102,10 +103,10 @@ async fn grid_filter_single_select_is_test() { async fn grid_filter_single_select_is_test2() { let mut test = DatabaseFilterTest::new().await; let field = test.get_first_field(FieldType::SingleSelect); - let rows = test.get_rows().await; + let row_details = test.get_rows().await; let mut options = test.get_single_select_type_option(&field.id).options; let option = options.remove(0); - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let scripts = vec![ CreateSingleSelectFilter { @@ -118,13 +119,13 @@ async fn grid_filter_single_select_is_test2() { }, AssertNumberOfVisibleRows { expected: 2 }, UpdateSingleSelectCell { - row_id: rows[1].id.clone(), + row_id: row_details[1].row.id.clone(), option_id: option.id.clone(), changed: None, }, AssertNumberOfVisibleRows { expected: 3 }, UpdateSingleSelectCell { - row_id: rows[1].id.clone(), + row_id: row_details[1].row.id.clone(), option_id: "".to_string(), changed: Some(FilterRowChanged { showing_num_of_rows: 0, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs index f04478adc0..134d143ff1 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs @@ -1,10 +1,11 @@ -use crate::database::filter_test::script::FilterScript::*; -use crate::database::filter_test::script::*; use flowy_database2::entities::{ FieldType, TextFilterConditionPB, TextFilterPB, UpdateFilterPayloadPB, }; use flowy_database2::services::filter::FilterType; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::*; + #[tokio::test] async fn grid_filter_text_is_empty_test() { let mut test = DatabaseFilterTest::new().await; @@ -87,7 +88,7 @@ async fn grid_filter_contain_text_test() { #[tokio::test] async fn grid_filter_contain_text_test2() { let mut test = DatabaseFilterTest::new().await; - let rows = test.rows.clone(); + let row_detail = test.row_details.clone(); let scripts = vec![ CreateTextFilter { @@ -99,7 +100,7 @@ async fn grid_filter_contain_text_test2() { }), }, UpdateTextCell { - row_id: rows[1].id.clone(), + row_id: row_detail[1].row.id.clone(), text: "ABC".to_string(), changed: Some(FilterRowChanged { showing_num_of_rows: 1, @@ -220,7 +221,7 @@ async fn grid_filter_delete_test() { #[tokio::test] async fn grid_filter_update_empty_text_cell_test() { let mut test = DatabaseFilterTest::new().await; - let rows = test.rows.clone(); + let row_details = test.row_details.clone(); let scripts = vec![ CreateTextFilter { condition: TextFilterConditionPB::TextIsEmpty, @@ -232,7 +233,7 @@ async fn grid_filter_update_empty_text_cell_test() { }, AssertFilterCount { count: 1 }, UpdateTextCell { - row_id: rows[0].id.clone(), + row_id: row_details[0].row.id.clone(), text: "".to_string(), changed: Some(FilterRowChanged { showing_num_of_rows: 1, diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index 1702b7a380..b69bbd68bc 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -2,7 +2,7 @@ use collab_database::database::gen_row_id; use collab_database::fields::Field; use collab_database::rows::{CreateRowParams, RowId}; -use flowy_database2::entities::{FieldType, GroupPB, RowPB}; +use flowy_database2::entities::{FieldType, GroupPB, RowMetaPB}; use flowy_database2::services::cell::{ delete_select_option_cell, insert_select_option_cell, insert_url_cell, }; @@ -27,7 +27,7 @@ pub enum GroupScript { AssertRow { group_index: usize, row_index: usize, - row: RowPB, + row: RowMetaPB, }, MoveRow { from_group_index: usize, @@ -260,7 +260,7 @@ impl DatabaseGroupTest { groups.get(index).unwrap().clone() } - pub async fn row_at_index(&self, group_index: usize, row_index: usize) -> RowPB { + pub async fn row_at_index(&self, group_index: usize, row_index: usize) -> RowMetaPB { let groups = self.group_at_index(group_index).await; groups.rows.get(row_index).unwrap().clone() } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index 75b88b26a9..ec11139c77 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -2,17 +2,19 @@ // #![allow(dead_code)] // #![allow(unused_imports)] -use crate::database::database_editor::TestRowBuilder; -use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER}; -use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData}; +use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; use collab_database::views::{DatabaseLayout, DatabaseView}; +use strum::IntoEnumIterator; + use flowy_database2::entities::FieldType; use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption; use flowy_database2::services::field::{ DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, }; -use strum::IntoEnumIterator; + +use crate::database::database_editor::TestRowBuilder; +use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER}; // Kanban board unit test mock data pub fn make_test_board() -> DatabaseData { @@ -118,7 +120,7 @@ pub fn make_test_board() -> DatabaseData { // We have many assumptions base on the number of the rows, so do not change the number of the loop. for i in 0..5 { - let mut row_builder = TestRowBuilder::new(i.into(), &fields); + let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); match i { 0 => { for field_type in FieldType::iter() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs index 0345acfd3a..95a2e44f0f 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs @@ -1,11 +1,12 @@ -use crate::database::database_editor::TestRowBuilder; -use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData}; +use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; +use strum::IntoEnumIterator; use flowy_database2::entities::FieldType; use flowy_database2::services::field::{FieldBuilder, MultiSelectTypeOption}; use flowy_database2::services::setting::CalendarLayoutSetting; -use strum::IntoEnumIterator; + +use crate::database::database_editor::TestRowBuilder; // Calendar unit test mock data pub fn make_test_calendar() -> DatabaseData { @@ -40,7 +41,7 @@ pub fn make_test_calendar() -> DatabaseData { let calendar_setting: LayoutSetting = CalendarLayoutSetting::new(date_field_id).into(); for i in 0..5 { - let mut row_builder = TestRowBuilder::new(i.into(), &fields); + let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); match i { 0 => { for field_type in FieldType::iter() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index 73ecdec6fb..7555953d80 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -1,16 +1,16 @@ -use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER}; -use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData}; - +use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; use collab_database::views::{DatabaseLayout, DatabaseView}; +use strum::IntoEnumIterator; -use crate::database::database_editor::TestRowBuilder; use flowy_database2::entities::FieldType; use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption; use flowy_database2::services::field::{ DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, }; -use strum::IntoEnumIterator; + +use crate::database::database_editor::TestRowBuilder; +use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER}; pub fn make_test_grid() -> DatabaseData { let mut fields = vec![]; @@ -117,7 +117,7 @@ pub fn make_test_grid() -> DatabaseData { } for i in 0..6 { - let mut row_builder = TestRowBuilder::new(i.into(), &fields); + let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); match i { 0 => { for field_type in FieldType::iter() { @@ -271,7 +271,7 @@ pub fn make_no_date_test_grid() -> DatabaseData { } for i in 0..3 { - let mut row_builder = TestRowBuilder::new(i.into(), &fields); + let mut row_builder = TestRowBuilder::new(gen_row_id(), &fields); match i { 0 => { for field_type in FieldType::iter() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index fcf0b187d3..88dafccade 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -1,10 +1,10 @@ -use crate::database::database_editor::DatabaseEditorTest; use flowy_database2::entities::FieldType; use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::field::CHECK; - use flowy_database2::services::share::csv::CSVFormat; +use crate::database::database_editor::DatabaseEditorTest; + #[tokio::test] async fn export_meta_csv_test() { let test = DatabaseEditorTest::new_grid().await; @@ -63,8 +63,8 @@ async fn export_and_then_import_meta_csv_test() { assert_eq!(fields[9].field_type, 9); for field in fields { - for (index, row) in rows.iter().enumerate() { - if let Some(cell) = row.cells.get(&field.id) { + for (index, row_detail) in rows.iter().enumerate() { + if let Some(cell) = row_detail.row.cells.get(&field.id) { let field_type = FieldType::from(field.field_type); let s = stringify_cell_data(cell, &field_type, &field_type, &field); match &field_type { @@ -102,7 +102,7 @@ async fn export_and_then_import_meta_csv_test() { } else { panic!( "Can not found the cell with id: {} in {:?}", - field.id, row.cells + field.id, row_detail.row.cells ); } } @@ -136,8 +136,8 @@ async fn history_database_import_test() { assert_eq!(fields[7].field_type, 7); for field in fields { - for (index, row) in rows.iter().enumerate() { - if let Some(cell) = row.cells.get(&field.id) { + for (index, row_detail) in rows.iter().enumerate() { + if let Some(cell) = row_detail.row.cells.get(&field.id) { let field_type = FieldType::from(field.field_type); let s = stringify_cell_data(cell, &field_type, &field_type, &field); match &field_type { @@ -183,7 +183,7 @@ async fn history_database_import_test() { } else { panic!( "Can not found the cell with id: {} in {:?}", - field.id, row.cells + field.id, row_detail.row.cells ); } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs index 544c416960..c2ce322d86 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs @@ -1,5 +1,4 @@ use std::cmp::min; - use std::time::Duration; use async_stream::stream; @@ -103,8 +102,8 @@ impl DatabaseSortTest { let rows = self.editor.get_rows(&self.view_id).await.unwrap(); let field = self.editor.get_field(&field_id).unwrap(); let field_type = FieldType::from(field.field_type); - for row in rows { - if let Some(cell) = row.cells.get(&field_id) { + for row_detail in rows { + if let Some(cell) = row_detail.row.cells.get(&field_id) { let content = stringify_cell_data(cell, &field_type, &field_type, &field); cells.push(content); } else { diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs index 7b338467f6..09022e8169 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs @@ -42,10 +42,10 @@ async fn sort_change_notification_by_update_text_test() { ]; test.run_scripts(scripts).await; - let rows = test.get_rows().await; + let row_details = test.get_rows().await; let scripts = vec![ UpdateTextCell { - row_id: rows[2].id.clone(), + row_id: row_details[2].row.id.clone(), text: "E".to_string(), }, AssertSortChanged { diff --git a/frontend/rust-lib/flowy-folder2/src/entities/view.rs b/frontend/rust-lib/flowy-folder2/src/entities/view.rs index 77f808f934..15ef8ed158 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/view.rs @@ -15,6 +15,8 @@ pub struct ViewPB { #[pb(index = 1)] pub id: String, + /// The parent view id of the view. + /// Each view should have a parent view except the orphan view. #[pb(index = 2)] pub parent_view_id: String, @@ -24,11 +26,22 @@ pub struct ViewPB { #[pb(index = 4)] pub create_time: i64, + /// Each view can have multiple child views. #[pb(index = 5)] pub child_views: Vec, + /// The layout of the view. It will be used to determine how the view should be rendered. #[pb(index = 6)] pub layout: ViewLayoutPB, + + /// The icon url of the view. + /// It can be used to save the emoji icon of the view. + #[pb(index = 7, one_of)] + pub icon_url: Option, + + /// The cover url of the view. + #[pb(index = 8, one_of)] + pub cover_url: Option, } pub fn view_pb_without_child_views(view: View) -> ViewPB { @@ -39,6 +52,8 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB { create_time: view.created_at, child_views: Default::default(), layout: view.layout.into(), + icon_url: view.icon_url, + cover_url: view.cover_url, } } @@ -54,6 +69,8 @@ pub fn view_pb_with_child_views(view: View, child_views: Vec) -> ViewPB { .map(view_pb_without_child_views) .collect(), layout: view.layout.into(), + icon_url: view.icon_url, + cover_url: view.cover_url, } } @@ -146,6 +163,27 @@ pub struct CreateViewPayloadPB { pub set_as_current: bool, } +/// The orphan view is meant to be a view that is not attached to any parent view. By default, this +/// view will not be shown in the view list unless it is attached to a parent view that is shown in +/// the view list. +#[derive(Default, ProtoBuf)] +pub struct CreateOrphanViewPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub name: String, + + #[pb(index = 3)] + pub desc: String, + + #[pb(index = 4)] + pub layout: ViewLayoutPB, + + #[pb(index = 5)] + pub initial_data: Vec, +} + #[derive(Debug, Clone)] pub struct CreateViewParams { pub parent_view_id: String, @@ -164,11 +202,11 @@ impl TryInto for CreateViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; - let belong_to_id = ViewIdentify::parse(self.parent_view_id)?.0; + let parent_view_id = ViewIdentify::parse(self.parent_view_id)?.0; let view_id = gen_view_id(); Ok(CreateViewParams { - parent_view_id: belong_to_id, + parent_view_id, name, desc: self.desc, layout: self.layout, @@ -180,6 +218,26 @@ impl TryInto for CreateViewPayloadPB { } } +impl TryInto for CreateOrphanViewPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let name = ViewName::parse(self.name)?.0; + let parent_view_id = ViewIdentify::parse(self.view_id.clone())?.0; + + Ok(CreateViewParams { + parent_view_id, + name, + desc: self.desc, + layout: self.layout, + view_id: self.view_id, + initial_data: self.initial_data, + meta: Default::default(), + set_as_current: false, + }) + } +} + #[derive(Default, ProtoBuf, Clone, Debug)] pub struct ViewIdPB { #[pb(index = 1)] @@ -227,6 +285,12 @@ pub struct UpdateViewPayloadPB { #[pb(index = 5, one_of)] pub layout: Option, + + #[pb(index = 6, one_of)] + pub icon_url: Option, + + #[pb(index = 7, one_of)] + pub cover_url: Option, } #[derive(Clone, Debug)] @@ -236,6 +300,12 @@ pub struct UpdateViewParams { pub desc: Option, pub thumbnail: Option, pub layout: Option, + + /// The icon url can be empty, which means the view has no icon. + pub icon_url: Option, + + /// The cover url can be empty, which means the view has no icon. + pub cover_url: Option, } impl TryInto for UpdateViewPayloadPB { @@ -265,6 +335,8 @@ impl TryInto for UpdateViewPayloadPB { desc, thumbnail, layout: self.layout.map(|ty| ty.into()), + icon_url: self.icon_url, + cover_url: self.cover_url, }) } } diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index f75bbf627c..39e2f33e93 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -3,13 +3,7 @@ use std::sync::Arc; use flowy_error::FlowyError; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use crate::entities::{ - view_pb_without_child_views, CreateViewParams, CreateViewPayloadPB, CreateWorkspaceParams, - CreateWorkspacePayloadPB, ImportPB, MoveViewParams, MoveViewPayloadPB, RepeatedTrashIdPB, - RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, RepeatedWorkspacePB, TrashIdPB, - UpdateViewParams, UpdateViewPayloadPB, ViewIdPB, ViewPB, WorkspaceIdPB, WorkspacePB, - WorkspaceSettingPB, -}; +use crate::entities::*; use crate::manager::Folder2Manager; use crate::share::ImportParams; @@ -96,6 +90,19 @@ pub(crate) async fn create_view_handler( data_result_ok(view_pb_without_child_views(view)) } +pub(crate) async fn create_orphan_view_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + let params: CreateViewParams = data.into_inner().try_into()?; + let set_as_current = params.set_as_current; + let view = folder.create_orphan_view_with_params(params).await?; + if set_as_current { + let _ = folder.set_current_view(&view.id).await; + } + data_result_ok(view_pb_without_child_views(view)) +} + pub(crate) async fn read_view_handler( data: AFPluginData, folder: AFPluginState>, diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index c8e384b3b7..652fef9b76 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -1,11 +1,12 @@ -use crate::event_handler::*; -use crate::manager::Folder2Manager; -use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; +use std::sync::Arc; +use strum_macros::Display; + +use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::*; -use std::sync::Arc; -use strum_macros::Display; +use crate::event_handler::*; +use crate::manager::Folder2Manager; pub fn init(folder: Arc) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) @@ -20,6 +21,7 @@ pub fn init(folder: Arc) -> AFPlugin { .event(FolderEvent::ReadWorkspaceViews, read_workspace_views_handler) // View .event(FolderEvent::CreateView, create_view_handler) + .event(FolderEvent::CreateOrphanView, create_orphan_view_handler) .event(FolderEvent::ReadView, read_view_handler) .event(FolderEvent::UpdateView, update_view_handler) .event(FolderEvent::DeleteView, delete_view_handler) @@ -89,6 +91,10 @@ pub enum FolderEvent { #[event(input = "ViewIdPB")] CloseView = 15, + /// Create a new view in the corresponding app + #[event(input = "CreateOrphanViewPayloadPB", output = "ViewPB")] + CreateOrphanView = 16, + #[event()] CopyLink = 20, diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 8e65968fc0..d9f238ad2d 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -261,6 +261,26 @@ impl Folder2Manager { Ok(view) } + /// The orphan view is meant to be a view that is not attached to any parent view. By default, this + /// view will not be shown in the view list unless it is attached to a parent view that is shown in + /// the view list. + pub async fn create_orphan_view_with_params( + &self, + params: CreateViewParams, + ) -> FlowyResult { + let view_layout: ViewLayout = params.layout.clone().into(); + let handler = self.get_handler(&view_layout)?; + let user_id = self.user.user_id()?; + handler + .create_built_in_view(user_id, ¶ms.view_id, ¶ms.name, view_layout.clone()) + .await?; + let view = create_view(params, view_layout); + self.with_folder((), |folder| { + folder.insert_view(view.clone()); + }); + Ok(view) + } + #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { let view = self @@ -391,6 +411,8 @@ impl Folder2Manager { .set_name_if_not_none(params.name) .set_desc_if_not_none(params.desc) .set_layout_if_not_none(params.layout) + .set_icon_url_if_not_none(params.icon_url) + .set_cover_url_if_not_none(params.cover_url) .done() }); Some((old_view, new_view)) diff --git a/frontend/rust-lib/flowy-folder2/src/view_operation.rs b/frontend/rust-lib/flowy-folder2/src/view_operation.rs index 4ed8ca7279..5deac30b0f 100644 --- a/frontend/rust-lib/flowy-folder2/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder2/src/view_operation.rs @@ -54,6 +54,8 @@ pub struct ViewBuilder { desc: String, layout: ViewLayout, child_views: Vec, + icon_url: Option, + cover_url: Option, } impl ViewBuilder { @@ -65,6 +67,8 @@ impl ViewBuilder { desc: Default::default(), layout: ViewLayout::Document, child_views: vec![], + icon_url: None, + cover_url: None, } } @@ -107,6 +111,8 @@ impl ViewBuilder { desc: self.desc, created_at: timestamp(), layout: self.layout, + icon_url: self.icon_url, + cover_url: self.cover_url, children: RepeatedView::new( self .child_views @@ -247,6 +253,8 @@ pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View children: Default::default(), created_at: time, layout, + icon_url: None, + cover_url: None, } } diff --git a/frontend/rust-lib/flowy-folder2/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder2/tests/workspace/script.rs index 6b192db1f9..dec2750ce5 100644 --- a/frontend/rust-lib/flowy-folder2/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder2/tests/workspace/script.rs @@ -266,8 +266,7 @@ pub async fn update_view( view_id: view_id.to_string(), name, desc, - thumbnail: None, - layout: None, + ..Default::default() }; EventBuilder::new(sdk.clone()) .event(UpdateView) diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index eff5c196d2..31b1731a1e 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -1,8 +1,8 @@ -use bytes::Bytes; use std::convert::TryFrom; use std::env::temp_dir; use std::sync::Arc; +use bytes::Bytes; use nanoid::nanoid; use parking_lot::RwLock; @@ -96,6 +96,16 @@ impl FlowyCoreTest { .await; } + pub async fn update_view(&self, changeset: UpdateViewPayloadPB) -> Option { + // delete the view. the view will be moved to trash + EventBuilder::new(self.clone()) + .event(flowy_folder2::event_map::FolderEvent::UpdateView) + .payload(changeset) + .async_send() + .await + .error() + } + pub async fn create_view(&self, parent_id: &str, name: String) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), @@ -264,12 +274,23 @@ impl FlowyCoreTest { .error() } + pub async fn get_primary_field(&self, database_view_id: &str) -> FieldPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetPrimaryField) + .payload(DatabaseViewIdPB { + value: database_view_id.to_string(), + }) + .async_send() + .await + .parse::() + } + pub async fn create_row( &self, view_id: &str, start_row_id: Option, data: Option, - ) -> RowPB { + ) -> RowMetaPB { EventBuilder::new(self.clone()) .event(flowy_database2::event_map::DatabaseEvent::CreateRow) .payload(CreateRowPayloadPB { @@ -280,7 +301,7 @@ impl FlowyCoreTest { }) .async_send() .await - .parse::() + .parse::() } pub async fn delete_row(&self, view_id: &str, row_id: &str) -> Option { @@ -309,6 +330,28 @@ impl FlowyCoreTest { .parse::() } + pub async fn get_row_meta(&self, view_id: &str, row_id: &str) -> RowMetaPB { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetRowMeta) + .payload(RowIdPB { + view_id: view_id.to_string(), + row_id: row_id.to_string(), + group_id: None, + }) + .async_send() + .await + .parse::() + } + + pub async fn update_row_meta(&self, changeset: UpdateRowMetaChangesetPB) -> Option { + EventBuilder::new(self.clone()) + .event(flowy_database2::event_map::DatabaseEvent::UpdateRowMeta) + .payload(changeset) + .async_send() + .await + .error() + } + pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option { EventBuilder::new(self.clone()) .event(flowy_database2::event_map::DatabaseEvent::DuplicateRow) diff --git a/frontend/rust-lib/flowy-test/tests/database/test.rs b/frontend/rust-lib/flowy-test/tests/database/test.rs index 835852f2e8..48d598604a 100644 --- a/frontend/rust-lib/flowy-test/tests/database/test.rs +++ b/frontend/rust-lib/flowy-test/tests/database/test.rs @@ -1,14 +1,15 @@ use std::convert::TryFrom; use bytes::Bytes; -use lib_infra::util::timestamp; use flowy_database2::entities::{ CellChangesetPB, CellIdPB, ChecklistCellDataChangesetPB, DatabaseLayoutPB, DatabaseSettingChangesetPB, DatabaseViewIdPB, DateChangesetPB, FieldType, SelectOptionCellDataPB, + UpdateRowMetaChangesetPB, }; use flowy_test::event_builder::EventBuilder; use flowy_test::FlowyCoreTest; +use lib_infra::util::timestamp; #[tokio::test] async fn get_database_id_event_test() { @@ -180,6 +181,19 @@ async fn duplicate_primary_field_test() { assert!(error.is_some()); } +#[tokio::test] +async fn get_primary_field_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // By default the primary field type is RichText. + let field = test.get_primary_field(&grid_view.id).await; + assert_eq!(field.field_type, FieldType::RichText); +} + #[tokio::test] async fn create_row_event_test() { let test = FlowyCoreTest::new_with_user().await; @@ -203,17 +217,91 @@ async fn delete_row_event_test() { // delete the row let database = test.get_database(&grid_view.id).await; - let error = test.delete_row(&grid_view.id, &database.rows[0].id).await; + let remove_row_id = database.rows[0].id.clone(); + assert_eq!(database.rows.len(), 3); + let error = test.delete_row(&grid_view.id, &remove_row_id).await; assert!(error.is_none()); let database = test.get_database(&grid_view.id).await; assert_eq!(database.rows.len(), 2); // get the row again and check if it is deleted. - let optional_row = test.get_row(&grid_view.id, &database.rows[0].id).await; + let optional_row = test.get_row(&grid_view.id, &remove_row_id).await; assert!(optional_row.row.is_none()); } +#[tokio::test] +async fn get_row_event_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + let row = test.get_row(&grid_view.id, &database.rows[0].id).await.row; + assert!(row.is_some()); + + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert!(!row.document_id.is_empty()); +} + +#[tokio::test] +async fn update_row_meta_event_with_url_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + // By default the row icon is None. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.icon, None); + + // Insert icon url to the row. + let changeset = UpdateRowMetaChangesetPB { + id: database.rows[0].id.clone(), + view_id: grid_view.id.clone(), + icon_url: Some("icon_url".to_owned()), + cover_url: None, + }; + let error = test.update_row_meta(changeset).await; + assert!(error.is_none()); + + // Check if the icon is updated. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.icon, Some("icon_url".to_owned())); +} + +#[tokio::test] +async fn update_row_meta_event_with_cover_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + // By default the row icon is None. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.cover, None); + + // Insert cover to the row. + let changeset = UpdateRowMetaChangesetPB { + id: database.rows[0].id.clone(), + view_id: grid_view.id.clone(), + cover_url: Some("cover url".to_owned()), + icon_url: None, + }; + let error = test.update_row_meta(changeset).await; + assert!(error.is_none()); + + // Check if the icon is updated. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.cover, Some("cover url".to_owned())); +} + #[tokio::test] async fn delete_row_event_with_invalid_row_id_test() { let test = FlowyCoreTest::new_with_user().await; diff --git a/frontend/rust-lib/flowy-test/tests/folder/test.rs b/frontend/rust-lib/flowy-test/tests/folder/test.rs index d2a987992d..1afc4f1d0e 100644 --- a/frontend/rust-lib/flowy-test/tests/folder/test.rs +++ b/frontend/rust-lib/flowy-test/tests/folder/test.rs @@ -61,6 +61,69 @@ async fn create_view_event_test() { assert_eq!(view.layout, ViewLayoutPB::Document); } +#[tokio::test] +async fn update_view_event_with_name_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let view = test + .create_view(¤t_workspace.id, "My first view".to_string()) + .await; + + let error = test + .update_view(UpdateViewPayloadPB { + view_id: view.id.clone(), + name: Some("My second view".to_string()), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + let view = test.get_view(&view.id).await; + assert_eq!(view.name, "My second view"); +} + +#[tokio::test] +async fn update_view_event_with_icon_url_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let view = test + .create_view(¤t_workspace.id, "My first view".to_string()) + .await; + + let error = test + .update_view(UpdateViewPayloadPB { + view_id: view.id.clone(), + icon_url: Some("appflowy.io".to_string()), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + let view = test.get_view(&view.id).await; + assert_eq!(view.icon_url.unwrap(), "appflowy.io"); +} + +#[tokio::test] +async fn update_view_event_with_cover_url_test() { + let test = FlowyCoreTest::new_with_user().await; + let current_workspace = test.get_current_workspace().await.workspace; + let view = test + .create_view(¤t_workspace.id, "My first view".to_string()) + .await; + + let error = test + .update_view(UpdateViewPayloadPB { + view_id: view.id.clone(), + cover_url: Some("appflowy.io".to_string()), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + let view = test.get_view(&view.id).await; + assert_eq!(view.cover_url.unwrap(), "appflowy.io"); +} + #[tokio::test] async fn delete_view_event_test() { let test = FlowyCoreTest::new_with_user().await;