From a7f40b2adcccd05faf7e80905baa2ecbfbf280e4 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:08:01 +0200 Subject: [PATCH] fix: paste in list (#5621) * fix: support pasting in list * test: add simple test * chore: remove debugPrint --- .../document_copy_and_paste_test.dart | 95 +++++++++++++------ .../copy_and_paste/custom_copy_command.dart | 3 +- .../copy_and_paste/custom_paste_command.dart | 16 ++-- .../editor_state_paste_node_extension.dart | 19 ++++ 4 files changed, 95 insertions(+), 38 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart index 0ea1391790..730d776460 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart @@ -1,10 +1,11 @@ import 'dart:io'; +import 'package:flutter/services.dart'; + import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -116,6 +117,53 @@ void main() { ]); }); }); + + testWidgets('paste bulleted list in numbered list', (tester) async { + const inAppJson = + '{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}'; + + await tester.pasteContent( + inAppJson: inAppJson, + beforeTest: (editorState) async { + final transaction = editorState.transaction; + // Insert two numbered list nodes + // 1. Parent One + // 2. + transaction.insertNodes( + [0], + [ + Node( + type: NumberedListBlockKeys.type, + attributes: { + 'delta': [ + {"insert": "One"}, + ], + }, + ), + Node( + type: NumberedListBlockKeys.type, + attributes: {'delta': []}, + ), + ], + ); + + // Set the selection to the second numbered list node (which has empty delta) + transaction.afterSelection = Selection.collapsed(Position(path: [1])); + + await editorState.apply(transaction); + await tester.pumpAndSettle(); + }, + (editorState) { + final secondNode = editorState.getNodeAtPath([1]); + expect(secondNode?.delta?.toPlainText(), 'Hello'); + expect(secondNode?.children.length, 1); + + final childNode = secondNode?.children.first; + expect(childNode?.delta?.toPlainText(), 'World'); + expect(childNode?.type, BulletedListBlockKeys.type); + }, + ); + }); }); testWidgets('paste image(png) from memory', (tester) async { @@ -210,42 +258,33 @@ void main() { testWidgets('paste the html content contains section', (tester) async { const html = '''
AppFlowy
Hello World
'''; - await tester.pasteContent( - html: html, - (editorState) { - expect(editorState.document.root.children.length, 2); - final node1 = editorState.getNodeAtPath([0])!; - final node2 = editorState.getNodeAtPath([1])!; - expect(node1.type, ParagraphBlockKeys.type); - expect(node2.type, ParagraphBlockKeys.type); - }, - ); + await tester.pasteContent(html: html, (editorState) { + expect(editorState.document.root.children.length, 2); + final node1 = editorState.getNodeAtPath([0])!; + final node2 = editorState.getNodeAtPath([1])!; + expect(node1.type, ParagraphBlockKeys.type); + expect(node2.type, ParagraphBlockKeys.type); + }); }); testWidgets('paste the html from google translation', (tester) async { const html = '''
new force
Assessment focus: potential motivations, empathy

➢Personality characteristics and potential motivations:
-Reflection of self-worth
-Need to be respected
-Have a unique definition of success
-Be true to your own lifestyle
'''; - await tester.pasteContent( - html: html, - (editorState) { - expect(editorState.document.root.children.length, 8); - }, - ); + await tester.pasteContent(html: html, (editorState) { + expect(editorState.document.root.children.length, 8); + }); }); testWidgets( 'auto convert url to link preview block', - (widgetTester) async { + (tester) async { const url = 'https://appflowy.io'; - await widgetTester.pasteContent( - plainText: url, - (editorState) { - expect(editorState.document.root.children.length, 2); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, LinkPreviewBlockKeys.type); - expect(node.attributes[LinkPreviewBlockKeys.url], url); - }, - ); + await tester.pasteContent(plainText: url, (editorState) { + expect(editorState.document.root.children.length, 2); + final node = editorState.getNodeAtPath([0])!; + expect(node.type, LinkPreviewBlockKeys.type); + expect(node.attributes[LinkPreviewBlockKeys.url], url); + }); }, ); } @@ -256,6 +295,7 @@ extension on WidgetTester { Future Function(EditorState editorState)? beforeTest, String? plainText, String? html, + String? inAppJson, (String, Uint8List?)? image, }) async { await initializeAppFlowy(); @@ -271,6 +311,7 @@ extension on WidgetTester { ClipboardServiceData( plainText: plainText, html: html, + inAppJson: inAppJson, image: image, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart index 37f019d750..8370a24b66 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart @@ -1,9 +1,10 @@ import 'dart:convert'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; /// Copy. /// diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index 0d04a2765f..53812fbfe4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -1,4 +1,6 @@ -import 'package:appflowy/plugins/document/application/prelude.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart'; @@ -8,12 +10,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; -/// Paste. -/// /// - support /// - desktop /// - web @@ -43,8 +42,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) { final image = data.image; // paste as link preview - final result = await _pasteAsLinkPreview(editorState, plainText); - if (result) { + if (await _pasteAsLinkPreview(editorState, plainText)) { return; } @@ -57,16 +55,14 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) { // try to paste the content in order, if any of them is failed, then try the next one if (inAppJson != null && inAppJson.isNotEmpty) { await editorState.deleteSelectionIfNeeded(); - final result = await editorState.pasteInAppJson(inAppJson); - if (result) { + if (await editorState.pasteInAppJson(inAppJson)) { return; } } if (html != null && html.isNotEmpty) { await editorState.deleteSelectionIfNeeded(); - final result = await editorState.pasteHtml(html); - if (result) { + if (await editorState.pasteHtml(html)) { return; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart index 34c6c8fe06..6cee6f1db7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart @@ -1,5 +1,11 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +final _listTypes = [ + BulletedListBlockKeys.type, + TodoListBlockKeys.type, + NumberedListBlockKeys.type, +]; + extension PasteNodes on EditorState { Future pasteSingleLineNode(Node insertedNode) async { final selection = await deleteSelectionIfNeeded(); @@ -28,6 +34,19 @@ extension PasteNodes on EditorState { offset: offset, ), ); + } else if (_listTypes.contains(node.type)) { + final convertedNode = insertedNode.copyWith(type: node.type); + final path = selection.start.path; + transaction + ..insertNode(path, convertedNode) + ..deleteNodesAtPath(path); + + // Set the afterSelection to the last child of the inserted node + final lastChildPath = calculatePath(path, [convertedNode]); + final lastChildOffset = calculateLength([convertedNode]); + transaction.afterSelection = Selection.collapsed( + Position(path: lastChildPath, offset: lastChildOffset), + ); } else if (insertedDelta != null) { // if the node is not empty, insert the delta from inserted node after the selection. transaction.insertTextDelta(node, selection.endIndex, insertedDelta);