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 =
'''''';
- 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 =
'''Assessment focus: potential motivations, empathy➢Personality characteristics and potential motivations:-Reflection of self-worth-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);