diff --git a/frontend/appflowy_flutter/integration_test/cover_image_test.dart b/frontend/appflowy_flutter/integration_test/cover_image_test.dart index a2bee4856f..fcb8a12ae7 100644 --- a/frontend/appflowy_flutter/integration_test/cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/cover_image_test.dart @@ -28,7 +28,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.hoverOnCoverPluginAddButton(); + await tester.editor.hoverOnCoverPluginAddButton(); tester.expectToSeePluginAddCoverAndIconButton(); }); diff --git a/frontend/appflowy_flutter/integration_test/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/document_with_database_test.dart new file mode 100644 index 0000000000..73fdf4dd51 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document_with_database_test.dart @@ -0,0 +1,110 @@ +import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('database view in document', () { + const location = 'database_view'; + + setUp(() async { + await TestFolder.cleanTestLocation(location); + await TestFolder.setTestLocation(location); + }); + + tearDown(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets('insert a referenced grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await insertReferenceDatabase(tester, ViewLayoutPB.Grid); + + // validate the referenced grid is inserted + expect( + find.descendant( + of: find.byType(AppFlowyEditor), + matching: find.byType(GridPage), + ), + findsOneWidget, + ); + }); + + testWidgets('insert a referenced board', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await insertReferenceDatabase(tester, ViewLayoutPB.Board); + + // validate the referenced board is inserted + expect( + find.descendant( + of: find.byType(AppFlowyEditor), + matching: find.byType(BoardPage), + ), + findsOneWidget, + ); + }); + + // testWidgets('insert a referenced calendar', (tester) async { + // await tester.initializeAppFlowy(); + // await tester.tapGoButton(); + + // await insertReferenceDatabase(tester, ViewLayoutPB.Calendar); + + // // validate the referenced grid is inserted + // expect( + // find.descendant( + // of: find.byType(AppFlowyEditor), + // matching: find.byType(CalendarPage), + // ), + // findsOneWidget, + // ); + // }); + }); +} + +/// Insert a referenced database of [layout] into the document +Future insertReferenceDatabase( + WidgetTester tester, + ViewLayoutPB layout, +) async { + // create a new grid + final id = uuid(); + final name = '${layout.name}_$id'; + await tester.createNewPageWithName( + layout, + name, + ); + // create a new document + await tester.createNewPageWithName( + ViewLayoutPB.Document, + 'insert_a_reference_${layout.name}', + ); + // tap the first line of the document + await tester.editor.tapLineOfEditorAt(0); + // insert a referenced grid + await tester.editor.showSlashMenu(); + await tester.editor.tapSlashMenuItemWithName( + layout.referencedMenuName, + ); + + final linkToPageMenu = find.byType(LinkToPageMenu); + expect(linkToPageMenu, findsOneWidget); + final referencedDatabase = find.descendant( + of: linkToPageMenu, + matching: find.findTextInFlowyText(name), + ); + expect(referencedDatabase, findsOneWidget); + await tester.tapButton(referencedDatabase); +} diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index 6ee18f424d..30879ba9fe 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -5,6 +5,7 @@ import 'document_test.dart' as document_test; import 'cover_image_test.dart' as cover_image_test; import 'share_markdown_test.dart' as share_markdown_test; import 'import_files_test.dart' as import_files_test; +import 'document_with_database_test.dart' as document_with_database_test; /// The main task runner for all integration tests in AppFlowy. /// @@ -20,6 +21,7 @@ void main() { document_test.main(); share_markdown_test.main(); import_files_test.main(); + document_with_database_test.main(); // board_test.main(); // empty_document_test.main(); // smart_menu_test.main(); diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index 2cc483138c..55035968ad 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -126,3 +126,11 @@ extension AppFlowyTestBase on WidgetTester { return; } } + +extension AppFlowyFinderTestBase on CommonFinders { + Finder findTextInFlowyText(String text) { + return find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == text, + ); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index c640e8512d..4c0d3a7b99 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -7,7 +7,7 @@ import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flutter/material.dart'; @@ -112,20 +112,32 @@ extension CommonOperations on WidgetTester { Future hoverOnWidget( Finder finder, { Offset? offset, + Future Function()? onHover, }) async { 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 (_) {} + await onHover?.call(); + await gesture.removePointer(); + } catch (err) { + Log.error('hoverOnWidget error: $err'); + } } /// Hover on the page name. - Future hoverOnPageName(String name) async { - await hoverOnWidget(findPageName(name)); + Future hoverOnPageName( + String name, { + Future Function()? onHover, + bool useLast = true, + }) async { + if (useLast) { + await hoverOnWidget(findPageName(name).last, onHover: onHover); + } else { + await hoverOnWidget(findPageName(name).first, onHover: onHover); + } } /// Tap the ... button beside the page name. @@ -137,24 +149,18 @@ extension CommonOperations on WidgetTester { } /// Tap the delete page button. - /// - /// Must call [tapPageOptionButton] first. Future tapDeletePageButton() async { await tapPageOptionButton(); await tapButtonWithName(ViewDisclosureAction.delete.name); } /// Tap the rename page button. - /// - /// Must call [tapPageOptionButton] first. Future tapRenamePageButton() async { await tapPageOptionButton(); await tapButtonWithName(ViewDisclosureAction.rename.name); } /// Rename the page. - /// - /// Must call [tapPageOptionButton] first. Future renamePage(String name) async { await tapRenamePageButton(); await enterText(find.byType(TextFormField), name); @@ -208,14 +214,50 @@ extension CommonOperations on WidgetTester { await tapButton(markdownButton); } - /// Hover on cover plugin button above the document - Future hoverOnCoverPluginAddButton() async { - final editor = find.byWidgetPredicate( - (widget) => widget is AppFlowyEditor, - ); - await hoverOnWidget( - editor, - offset: getTopLeft(editor).translate(20, 20), + Future createNewPageWithName(ViewLayoutPB layout, String name) async { + // create a new page + await tapAddButton(); + await tapButtonWithName(layout.menuName); + await pumpAndSettle(); + + // hover on it and change it's name + await hoverOnPageName( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + onHover: () async { + await renamePage(name); + await pumpAndSettle(); + }, ); + await pumpAndSettle(); + } +} + +extension ViewLayoutPBTest on ViewLayoutPB { + String get menuName { + switch (this) { + case ViewLayoutPB.Grid: + return LocaleKeys.grid_menuName.tr(); + case ViewLayoutPB.Board: + return LocaleKeys.board_menuName.tr(); + case ViewLayoutPB.Document: + return LocaleKeys.document_menuName.tr(); + case ViewLayoutPB.Calendar: + return LocaleKeys.calendar_menuName.tr(); + default: + throw UnsupportedError('Unsupported layout: $this'); + } + } + + String get referencedMenuName { + switch (this) { + case ViewLayoutPB.Grid: + return LocaleKeys.document_plugins_referencedGrid.tr(); + case ViewLayoutPB.Board: + return LocaleKeys.document_plugins_referencedBoard.tr(); + case ViewLayoutPB.Calendar: + return LocaleKeys.document_plugins_referencedCalendar.tr(); + default: + throw UnsupportedError('Unsupported layout: $this'); + } } } diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart new file mode 100644 index 0000000000..70b089370c --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -0,0 +1,45 @@ +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:flutter_test/flutter_test.dart'; + +import 'ime.dart'; +import 'util.dart'; + +extension EditorWidgetTester on WidgetTester { + EditorOperations get editor => EditorOperations(this); +} + +class EditorOperations { + const EditorOperations(this.tester); + + final WidgetTester tester; + + /// Tap the line of editor at [index] + Future tapLineOfEditorAt(int index) async { + final textBlocks = find.byType(TextBlockComponentWidget); + await tester.tapAt(tester.getTopRight(textBlocks.at(index))); + } + + /// Hover on cover plugin button above the document + Future hoverOnCoverPluginAddButton() async { + final editor = find.byWidgetPredicate( + (widget) => widget is AppFlowyEditor, + ); + await tester.hoverOnWidget( + editor, + offset: tester.getTopLeft(editor).translate(20, 20), + ); + } + + /// trigger the slash command (selection menu) + Future showSlashMenu() async { + await tester.ime.insertCharacter('/'); + } + + /// Tap the slash menu item with [name] + /// + /// Must call [showSlashMenu] first. + Future tapSlashMenuItemWithName(String name) async { + final slashMenuItem = find.text(name, findRichText: true); + await tester.tapButton(slashMenuItem); + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/util.dart b/frontend/appflowy_flutter/integration_test/util/util.dart index 2ff0cf6581..24efa3f5fb 100644 --- a/frontend/appflowy_flutter/integration_test/util/util.dart +++ b/frontend/appflowy_flutter/integration_test/util/util.dart @@ -3,3 +3,4 @@ export 'common_operations.dart'; export 'settings.dart'; export 'data.dart'; export 'expectation.dart'; +export 'editor_test_operations.dart'; 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 index 9aa55963d1..1d2b2771be 100644 --- 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 @@ -1,6 +1,7 @@ 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/editor_style.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'; @@ -102,11 +103,18 @@ class _RowEditorState extends State { return const SizedBox.shrink(); } return IntrinsicHeight( - child: AppFlowyEditorPage( - shrinkWrap: true, - autoFocus: false, - editorState: editorState, - scrollController: widget.scrollController, + child: Container( + constraints: const BoxConstraints(minHeight: 300), + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, + editorState: editorState, + scrollController: widget.scrollController, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.symmetric(horizontal: 10), + ), + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 04a8279a00..ecebf0bf8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/export_page_widget.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/base64_string.dart'; @@ -90,6 +91,10 @@ class _DocumentPageState extends State { Widget _buildEditorPage(BuildContext context, DocumentState state) { final appflowyEditorPage = AppFlowyEditorPage( editorState: editorState!, + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.symmetric(horizontal: 50), + ), header: _buildCoverAndIcon(context), ); return Column( 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 c26369e1ed..0227a47b67 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -17,6 +17,7 @@ class AppFlowyEditorPage extends StatefulWidget { this.shrinkWrap = false, this.scrollController, this.autoFocus, + required this.styleCustomizer, }); final Widget? header; @@ -24,6 +25,7 @@ class AppFlowyEditorPage extends StatefulWidget { final ScrollController? scrollController; final bool shrinkWrap; final bool? autoFocus; + final EditorStyleCustomizer styleCustomizer; @override State createState() => _AppFlowyEditorPageState(); @@ -91,9 +93,7 @@ class _AppFlowyEditorPageState extends State { style: styleCustomizer.selectionMenuStyleBuilder(), ).handler; - EditorStyleCustomizer get styleCustomizer => EditorStyleCustomizer( - context: context, - ); + EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer; DocumentBloc get documentBloc => context.read(); @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 2d4f247f3d..14a3afd0be 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -23,11 +23,13 @@ void showLinkToPageMenu( final top = alignment == Alignment.bottomLeft ? offset.dy : null; final bottom = alignment == Alignment.topLeft ? offset.dy : null; + keepEditorFocusNotifier.value += 1; late OverlayEntry linkToPageMenuEntry; linkToPageMenuEntry = FullScreenOverlayEntry( top: top, bottom: bottom, left: offset.dx, + dismissCallback: () => keepEditorFocusNotifier.value -= 1, builder: (context) => Material( color: Colors.transparent, child: LinkToPageMenu( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart index 8962a0987f..64d92e1125 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_node_widget.dart @@ -4,7 +4,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/change_cover_popover.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_popover.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide FlowySvg; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -164,8 +163,8 @@ class _AddCoverButtonState extends State<_AddCoverButton> { height: widget.hasIcon ? 180 : 50.0, alignment: Alignment.bottomLeft, width: double.infinity, - padding: EdgeInsets.only( - left: EditorStyleCustomizer.horizontalPadding + 30, + padding: const EdgeInsets.only( + left: 80, top: 20, bottom: 5, ), @@ -333,7 +332,7 @@ class _CoverImageState extends State<_CoverImage> { ), hasIcon ? Positioned( - left: EditorStyleCustomizer.horizontalPadding + 30, + left: 80, bottom: !hasCover ? 30 : 40, child: AppFlowyPopover( offset: const Offset(100, 0), @@ -416,7 +415,7 @@ class _CoverImageState extends State<_CoverImage> { Widget _buildCoverOverlayButtons(BuildContext context) { return Positioned( bottom: 20, - right: EditorStyleCustomizer.horizontalPadding, + right: 50, child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart index db8ac99a7b..ac77f9951a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/emoji_picker/emoji_menu_item.dart @@ -35,10 +35,12 @@ void showEmojiPickerMenu( final top = alignment == Alignment.bottomLeft ? offset.dy : null; final bottom = alignment == Alignment.topLeft ? offset.dy : null; + keepEditorFocusNotifier.value += 1; final emojiPickerMenuEntry = FullScreenOverlayEntry( top: top, bottom: bottom, left: offset.dx, + dismissCallback: () => keepEditorFocusNotifier.value -= 1, builder: (context) => Material( child: Container( width: 300, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 32344dde28..39240591a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -8,12 +8,11 @@ import 'package:google_fonts/google_fonts.dart'; class EditorStyleCustomizer { EditorStyleCustomizer({ required this.context, + required this.padding, }); - static double get horizontalPadding => - PlatformExtension.isDesktop ? 50.0 : 10.0; - final BuildContext context; + final EdgeInsets padding; EditorStyle style() { if (PlatformExtension.isDesktopOrWeb) { @@ -28,7 +27,7 @@ class EditorStyleCustomizer { final theme = Theme.of(context); final fontSize = context.read().state.fontSize; return EditorStyle.desktop( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: padding, backgroundColor: theme.colorScheme.surface, cursorColor: theme.colorScheme.primary, textStyleConfiguration: TextStyleConfiguration( @@ -65,7 +64,7 @@ class EditorStyleCustomizer { final theme = Theme.of(context); final fontSize = context.read().state.fontSize; return EditorStyle.desktop( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + padding: padding, backgroundColor: theme.colorScheme.surface, cursorColor: theme.colorScheme.primary, textStyleConfiguration: TextStyleConfiguration( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 69455a0985..8061ceaacd 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -52,12 +52,11 @@ packages: appflowy_editor: dependency: "direct main" description: - path: "." - ref: "23bc6d2" - resolved-ref: "23bc6d2f58ab7ab4ff21c507d53753de35094ec0" - url: "https://github.com/AppFlowy-IO/appflowy-editor.git" - source: git - version: "1.0.2" + name: appflowy_editor + sha256: "19c2567e23bbd8894243b2e57fa8436e3192c8dcb50c23499b6aea90a674a045" + url: "https://pub.dev" + source: hosted + version: "1.0.3" appflowy_popover: dependency: "direct main" description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 023ece27c1..85600bfbe1 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.2 - appflowy_editor: - git: - url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 23bc6d2 + appflowy_editor: ^1.0.3 + # appflowy_editor: + # git: + # url: https://github.com/AppFlowy-IO/appflowy-editor.git + # ref: d2460c9 appflowy_popover: path: packages/appflowy_popover