diff --git a/frontend/appflowy_flutter/integration_test/document/document_option_action_test.dart b/frontend/appflowy_flutter/integration_test/document/document_option_action_test.dart new file mode 100644 index 0000000000..66d77ef1f8 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/document/document_option_action_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // +, ... button beside the block component. + group('document with option action button', () { + testWidgets( + 'click + to add a block after current selection, and click + and option key to add a block before current selection', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + var editorState = tester.editor.getCurrentEditorState(); + expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isNotEmpty); + + // add a new block after the current selection + await tester.editor.hoverAndClickOptionAddButton([0], false); + // await tester.pumpAndSettle(); + expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isEmpty); + + // cancel the selection menu + await tester.tapAt(Offset.zero); + + await tester.editor.hoverAndClickOptionAddButton([0], true); + await tester.pumpAndSettle(); + expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty); + // cancel the selection menu + await tester.tapAt(Offset.zero); + await tester.tapAt(Offset.zero); + + await tester.createNewPageWithName(name: 'test'); + await tester.openPage(gettingStarted); + + // check the status again + editorState = tester.editor.getCurrentEditorState(); + expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty); + expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart index a29f8df49d..0514c06ef1 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_test_runner.dart @@ -5,6 +5,7 @@ import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test; import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test; import 'document_create_and_delete_test.dart' as document_create_and_delete_test; +import 'document_option_action_test.dart' as document_option_action_test; import 'document_text_direction_test.dart' as document_text_direction_test; import 'document_with_cover_image_test.dart' as document_with_cover_image_test; import 'document_with_database_test.dart' as document_with_database_test; @@ -31,4 +32,5 @@ void startTesting() { document_codeblock_paste_test.main(); document_alignment_test.main(); document_text_direction_test.main(); + document_option_action_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart index 886b4a2b16..97ea1cdaf4 100644 --- a/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/editor_test_operations.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; @@ -174,4 +175,34 @@ class EditorOperations { ); await tester.pumpAndSettle(const Duration(milliseconds: 200)); } + + /// hover and click on the + button beside the block component. + Future hoverAndClickOptionAddButton( + Path path, + bool withModifiedKey, // alt on windows or linux, option on macos + ) async { + final optionAddButton = find.byWidgetPredicate( + (widget) => + widget is BlockComponentActionWrapper && + widget.node.path.equals(path), + ); + await tester.hoverOnWidget( + optionAddButton, + onHover: () async { + if (withModifiedKey) { + await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); + } + await tester.tapButton( + find.byWidgetPredicate( + (widget) => + widget is BlockAddButton && + widget.blockComponentContext.node.path.equals(path), + ), + ); + if (withModifiedKey) { + await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); + } + }, + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index 11084ae424..666501c06b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -119,12 +119,15 @@ extension on InsertOperation { final parentId = node.parent?.id ?? editorState.getNodeAtPath(currentPath.parent)?.id ?? ''; - var prevId = previousNode?.id ?? - editorState.getNodeAtPath(currentPath.previous)?.id ?? - ''; + var prevId = previousNode?.id; + // if the node is the first child of the parent, then its prevId should be empty. + final isFirstChild = currentPath.previous.equals(currentPath); + if (!isFirstChild) { + prevId ??= editorState.getNodeAtPath(currentPath.previous)?.id ?? ''; + } + prevId ??= ''; assert(parentId.isNotEmpty); - if (currentPath.equals(currentPath.previous) && - !currentPath.equals([0])) { + if (isFirstChild) { prevId = ''; } else { assert(prevId.isNotEmpty && prevId != node.id); 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 a49263a8bf..5114103270 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -177,12 +177,14 @@ class _AppFlowyEditorPageState extends State { focusedSelection: selection, // setup the theme editorStyle: styleCustomizer.style(), - // customize the block builder + // customize the block builders blockComponentBuilders: blockComponentBuilders, // customize the shortcuts characterShortcutEvents: characterShortcutEvents, commandShortcutEvents: commandShortcutEvents, + // customize the context menu items contextMenuItems: customContextMenuItems, + // customize the header and footer. header: widget.header, footer: const VSpace(200), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index 71a785df12..fe6209508a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -243,8 +243,10 @@ class ColorOptionAction extends PopoverActionCell { final bgColor = node.attributes[blockComponentBackgroundColor] as String?; final selectedColor = bgColor?.tryToColor(); - // get default background color from themeExtension - final defaultColor = AFThemeExtension.of(context).calloutBGColor; + // get default background color for callout block from themeExtension + final defaultColor = node.type == CalloutBlockKeys.type + ? AFThemeExtension.of(context).calloutBGColor + : Colors.transparent; final colors = [ // reset to default background color FlowyColorOption( diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 90c8a3d81a..d63186c9df 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -54,11 +54,11 @@ packages: dependency: "direct main" description: path: "." - ref: "8e618465258b3de0ce5253c4fa97bacb24884e8c" - resolved-ref: "8e618465258b3de0ce5253c4fa97bacb24884e8c" + ref: ce8aa6d + resolved-ref: ce8aa6d35decc0c2e770f8440413ca73ae4a578b url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "1.4.1" + version: "1.4.2" appflowy_popover: dependency: "direct main" description: @@ -1575,6 +1575,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" universal_io: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index c3497677cd..28cba1e281 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: 8e618465258b3de0ce5253c4fa97bacb24884e8c + ref: ce8aa6d appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart index 230bf20613..3649a3b613 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart @@ -38,7 +38,7 @@ void main() { ); expect( actions[0].blockActionPB.payload.prevId, - editorState.document.root.children.first.id, + '', reason: '0 - prev id', ); expect(