mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-11-02 11:04:02 +00:00
feat: keep link format when converting preview block to text (#6435)
* feat: keep link format when converting preview block to text * test: add test * fix: flutter analyze * feat: ctrl/cmd+z to revert the link prevew op * test: add test * chore: update toast style
This commit is contained in:
parent
eca495ce63
commit
46e45c3715
@ -1,12 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.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:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
@ -311,13 +316,75 @@ void main() {
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) {
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
final convertToLinkButton = find.byWidgetPredicate((widget) {
|
||||
return widget is MenuBlockButton &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
|
||||
});
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tap(convertToLinkButton);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
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.editor.tapLineOfEditorAt(0);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -49,7 +49,13 @@ class BlockActionBottomSheet extends StatelessWidget {
|
||||
FlowyOptionTile.text(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s),
|
||||
leftIcon: const Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.copy_s,
|
||||
size: Size.square(16),
|
||||
),
|
||||
),
|
||||
onTap: () => onAction(BlockActionBottomSheetType.duplicate),
|
||||
),
|
||||
|
||||
@ -59,7 +65,8 @@ class BlockActionBottomSheet extends StatelessWidget {
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.m_delete_s,
|
||||
FlowySvgs.trash_s,
|
||||
size: const Size.square(18),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
|
||||
@ -117,12 +117,14 @@ Future<bool> _pasteAsLinkPreview(
|
||||
// 1. the url should contains a protocol
|
||||
// 2. the url should not be an image url
|
||||
if (text == null ||
|
||||
!isURL(text, {'require_protocol': true}) ||
|
||||
text.isImageUrl()) {
|
||||
text.isImageUrl() ||
|
||||
!isURL(text, {'require_protocol': true})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final selection = editorState.selection;
|
||||
// Apply the update only when the selection is collapsed
|
||||
// and at the start of the current line
|
||||
if (selection == null ||
|
||||
!selection.isCollapsed ||
|
||||
selection.startIndex != 0) {
|
||||
@ -130,18 +132,45 @@ Future<bool> _pasteAsLinkPreview(
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
// Apply the update only when the current node is a paragraph
|
||||
// and the paragraph is empty
|
||||
if (node == null ||
|
||||
node.type != ParagraphBlockKeys.type ||
|
||||
node.delta?.toPlainText().isNotEmpty == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode(
|
||||
selection.start.path,
|
||||
linkPreviewNode(url: text),
|
||||
// 1. insert the text with link format
|
||||
// 2. convert it the link preview node
|
||||
final textTransaction = editorState.transaction;
|
||||
textTransaction.insertText(
|
||||
node,
|
||||
0,
|
||||
text,
|
||||
attributes: {AppFlowyRichTextKeys.href: text},
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
await editorState.apply(
|
||||
textTransaction,
|
||||
skipHistoryDebounce: true,
|
||||
);
|
||||
|
||||
final linkPreviewTransaction = editorState.transaction;
|
||||
final insertedNodes = [
|
||||
linkPreviewNode(url: text),
|
||||
// if the next node is null, insert a empty paragraph node
|
||||
if (node.next == null) paragraphNode(),
|
||||
];
|
||||
linkPreviewTransaction.insertNodes(
|
||||
selection.start.path,
|
||||
insertedNodes,
|
||||
);
|
||||
linkPreviewTransaction.deleteNode(node);
|
||||
linkPreviewTransaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: node.path.next,
|
||||
),
|
||||
);
|
||||
await editorState.apply(linkPreviewTransaction);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -123,7 +123,10 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
node: node,
|
||||
editorState: context.read<EditorState>(),
|
||||
extendActionWidgets: _buildExtendActionWidgets(context),
|
||||
child: child,
|
||||
child: GestureDetector(
|
||||
onTap: () => afLaunchUrlString(url),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -134,8 +137,8 @@ class CustomLinkPreviewWidget extends StatelessWidget {
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(),
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.m_aa_link_s,
|
||||
size: Size.square(20),
|
||||
FlowySvgs.m_toolbar_link_m,
|
||||
size: Size.square(18),
|
||||
),
|
||||
onTap: () {
|
||||
context.pop();
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.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/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../image/custom_image_block_component/custom_image_block_component.dart';
|
||||
@ -50,8 +49,8 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
|
||||
const HSpace(4),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(),
|
||||
iconData: FlowySvgs.m_aa_link_s,
|
||||
onTap: () => convertUrlPreviewNodeToLink(
|
||||
iconData: FlowySvgs.m_toolbar_link_m,
|
||||
onTap: () async => convertUrlPreviewNodeToLink(
|
||||
context.read<EditorState>(),
|
||||
widget.node,
|
||||
),
|
||||
@ -65,7 +64,7 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
|
||||
const _Divider(),
|
||||
MenuBlockButton(
|
||||
tooltip: LocaleKeys.button_delete.tr(),
|
||||
iconData: FlowySvgs.delete_s,
|
||||
iconData: FlowySvgs.trash_s,
|
||||
onTap: deleteLinkPreviewNode,
|
||||
),
|
||||
const HSpace(4),
|
||||
@ -78,9 +77,9 @@ class _LinkPreviewMenuState extends State<LinkPreviewMenu> {
|
||||
final url = widget.node.attributes[CustomImageBlockKeys.url];
|
||||
if (url != null) {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
showSnackBarMessage(
|
||||
showToastNotification(
|
||||
context,
|
||||
LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(),
|
||||
message: LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,25 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
|
||||
void convertUrlPreviewNodeToLink(EditorState editorState, Node node) {
|
||||
assert(node.type == LinkPreviewBlockKeys.type);
|
||||
Future<void> convertUrlPreviewNodeToLink(
|
||||
EditorState editorState,
|
||||
Node node,
|
||||
) async {
|
||||
if (node.type != LinkPreviewBlockKeys.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
final url = node.attributes[ImageBlockKeys.url];
|
||||
final delta = Delta()
|
||||
..insert(
|
||||
url,
|
||||
attributes: {
|
||||
AppFlowyRichTextKeys.href: url,
|
||||
},
|
||||
);
|
||||
final transaction = editorState.transaction;
|
||||
transaction
|
||||
..insertNode(node.path, paragraphNode(text: url))
|
||||
..insertNode(node.path, paragraphNode(delta: delta))
|
||||
..deleteNode(node);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
@ -14,5 +27,5 @@ void convertUrlPreviewNodeToLink(EditorState editorState, Node node) {
|
||||
offset: url.length,
|
||||
),
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
return editorState.apply(transaction);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user