From 05c1924940e7dcd232067399ec505655cfc811c0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 14 Jan 2025 21:04:00 +0800 Subject: [PATCH] chore: update CHANGELOG.md (#7209) * Revert "fix: disable deleting mutilple nodes in table" This reverts commit 0507c39863c0a9e1e90a0c3ae4d77dda6d0daf9f. * chore: bump version 0.8.1 * chore: remove unused tests --- CHANGELOG.md | 12 + .../document_with_image_block_test.dart | 92 ----- .../settings/shortcuts_settings_test.dart | 2 +- .../custom_image_block_component.dart | 3 +- .../image_menu.dart | 35 +- .../shortcuts/backspace_command.dart | 321 ------------------ .../shortcuts/command_shortcuts.dart | 3 - 7 files changed, 34 insertions(+), 434 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7887bf371870d..d059347064fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ # Release Notes +## Version 0.8.1 - 14/01/2025 +### New Features: +- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only +- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat +- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language +- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more +- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar +- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile +### Bug Fixes +- Resolved an icon rendering issue in callout blocks, tab bars, and search results +- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails + ## Version 0.8.0 - 06/01/2025 ### Bug Fixes - Fixed error displaying in the page style menu diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart index 2e6d8959c12c5..3dcd6be8ae61c 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart @@ -7,19 +7,16 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu, ResizableImage; 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:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; -import 'package:run_with_network_images/run_with_network_images.dart'; import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; @@ -29,58 +26,6 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('image block in document', () { - Future testEmbedImage(WidgetTester tester, String url) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(), - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); - expect(find.byType(CustomImageBlockComponent), findsOneWidget); - expect(find.byType(ImagePlaceholder), findsOneWidget); - expect( - find.descendant( - of: find.byType(ImagePlaceholder), - matching: find.byType(AppFlowyPopover), - ), - findsOneWidget, - ); - expect(find.byType(UploadImageMenu), findsOneWidget); - - await tester.tapButtonWithName( - LocaleKeys.document_imageBlock_embedLink_label.tr(), - ); - await tester.enterText( - find.descendant( - of: find.byType(EmbedImageUrlWidget), - matching: find.byType(TextField), - ), - url, - ); - await tester.tapButton( - find.descendant( - of: find.byType(EmbedImageUrlWidget), - matching: find.text( - LocaleKeys.document_imageBlock_embedLink_label.tr(), - findRichText: true, - ), - ), - ); - await tester.pumpAndSettle(); - expect(find.byType(ResizableImage), findsOneWidget); - final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], url); - } - testWidgets('insert an image from local file', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -131,43 +76,6 @@ void main() { file.deleteSync(); }); - testWidgets('insert a gif image from network', (tester) async { - await testEmbedImage( - tester, - 'https://www.easygifanimator.net/images/samples/sparkles.gif', - ); - }); - - testWidgets('insert an image from unsplash', (tester) async { - await runWithNetworkImages(() async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(), - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); - expect(find.byType(CustomImageBlockComponent), findsOneWidget); - expect(find.byType(ImagePlaceholder), findsOneWidget); - expect( - find.descendant( - of: find.byType(ImagePlaceholder), - matching: find.byType(AppFlowyPopover), - ), - findsOneWidget, - ); - expect(find.byType(UploadImageMenu), findsOneWidget); - expect(find.text('Unsplash'), findsOneWidget); - }); - }); - testWidgets('insert two images from local file at once', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart index af2e42c2b7b3e..fe91becba6c64 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart @@ -87,7 +87,7 @@ void main() { as ShortcutSettingTile; expect( second.command.command, - 'backspace, shift+backspace', + '', ); }); }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart index e6c4b3347256a..12d7fd4da6c9b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart @@ -252,7 +252,6 @@ class CustomImageBlockComponentState extends State child: ValueListenableBuilder( valueListenable: showActionsNotifier, builder: (_, value, child) { - final url = node.attributes[CustomImageBlockKeys.url]; return Stack( children: [ editorState.editable @@ -266,7 +265,7 @@ class CustomImageBlockComponentState extends State child: child!, ) : child!, - if (value && url.isNotEmpty == true) + if (value) widget.menuBuilder!(widget.node, this, imageStateNotifier), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart index d5b02e755deb8..21caa81297ada 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart @@ -42,11 +42,12 @@ class _ImageMenuState extends State { @override Widget build(BuildContext context) { + final isPlaceholder = url == null || url!.isEmpty; final theme = Theme.of(context); return ValueListenableBuilder( valueListenable: widget.imageStateNotifier, builder: (_, state, child) { - if (state == ResizableImageState.loading) { + if (state == ResizableImageState.loading && !isPlaceholder) { return const SizedBox.shrink(); } @@ -66,21 +67,25 @@ class _ImageMenuState extends State { child: Row( children: [ const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(), - iconData: FlowySvgs.full_view_s, - onTap: openFullScreen, - ), - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.editor_copy.tr(), - iconData: FlowySvgs.copy_s, - onTap: copyImageLink, - ), - const HSpace(4), + if (!isPlaceholder) ...[ + MenuBlockButton( + tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(), + iconData: FlowySvgs.full_view_s, + onTap: openFullScreen, + ), + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.editor_copy.tr(), + iconData: FlowySvgs.copy_s, + onTap: copyImageLink, + ), + const HSpace(4), + ], if (widget.state.editorState.editable) ...[ - _ImageAlignButton(node: widget.node, state: widget.state), - const _Divider(), + if (!isPlaceholder) ...[ + _ImageAlignButton(node: widget.node, state: widget.state), + const _Divider(), + ], MenuBlockButton( tooltip: LocaleKeys.button_delete.tr(), iconData: FlowySvgs.trash_s, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart deleted file mode 100644 index 58c3d2fb5d6f8..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart +++ /dev/null @@ -1,321 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -/// Backspace key event. -/// -/// - support -/// - desktop -/// - web -/// - mobile -/// -final CommandShortcutEvent customBackspaceCommand = CommandShortcutEvent( - key: 'backspace', - getDescription: () => AppFlowyEditorL10n.current.cmdDeleteLeft, - command: 'backspace, shift+backspace', - handler: _backspaceCommandHandler, -); - -CommandShortcutEventHandler _backspaceCommandHandler = (editorState) { - final selection = editorState.selection; - final selectionType = editorState.selectionType; - - if (selection == null) { - return KeyEventResult.ignored; - } - - final reason = editorState.selectionUpdateReason; - - if (selectionType == SelectionType.block) { - return _backspaceInBlockSelection(editorState); - } else if (selection.isCollapsed) { - return _backspaceInCollapsedSelection(editorState); - } else if (reason == SelectionUpdateReason.selectAll) { - return _backspaceInSelectAll(editorState); - } else { - return _backspaceInNotCollapsedSelection(editorState); - } -}; - -/// Handle backspace key event when selection is collapsed. -CommandShortcutEventHandler _backspaceInCollapsedSelection = (editorState) { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return KeyEventResult.ignored; - } - - final position = selection.start; - final node = editorState.getNodeAtPath(position.path); - if (node == null) { - return KeyEventResult.ignored; - } - - final transaction = editorState.transaction; - - // delete the entire node if the delta is empty - if (node.delta == null) { - transaction.deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position( - path: position.path, - ), - ); - editorState.apply(transaction); - return KeyEventResult.handled; - } - - // Why do we use prevRunPosition instead of the position start offset? - // Because some character's length > 1, for example, emoji. - final index = node.delta!.prevRunePosition(position.offset); - - if (index < 0) { - // move this node to it's parent in below case. - // the node's next is null - // and the node's children is empty - if (node.next == null && - node.children.isEmpty && - node.parent?.parent != null && - node.parent?.delta != null) { - final path = node.parent!.path.next; - transaction - ..deleteNode(node) - ..insertNode(path, node) - ..afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - } else { - // If the deletion crosses columns and starts from the beginning position - // skip the node deletion process - // otherwise it will cause an error in table rendering. - if (node.parent?.type == SimpleTableCellBlockKeys.type && - position.offset == 0) { - return KeyEventResult.handled; - } - - final Node? tableParent = node - .findParent((element) => element.type == SimpleTableBlockKeys.type); - Node? prevTableParent; - final prev = node.previousNodeWhere((element) { - prevTableParent = element - .findParent((element) => element.type == SimpleTableBlockKeys.type); - // break if only one is in a table or they're in different tables - return tableParent != prevTableParent || - // merge with the previous node contains delta. - element.delta != null; - }); - // table nodes should be deleted using the table menu - // in-table paragraphs should only be deleted inside the table - if (prev != null && tableParent == prevTableParent) { - assert(prev.delta != null); - transaction - ..mergeText(prev, node) - ..insertNodes( - // insert children to previous node - prev.path.next, - node.children.toList(), - ) - ..deleteNode(node) - ..afterSelection = Selection.collapsed( - Position( - path: prev.path, - offset: prev.delta!.length, - ), - ); - } else { - // do nothing if there is no previous node contains delta. - return KeyEventResult.ignored; - } - } - } else { - // Although the selection may be collapsed, - // its length may not always be equal to 1 because some characters have a length greater than 1. - transaction.deleteText( - node, - index, - position.offset - index, - ); - } - - editorState.apply(transaction); - return KeyEventResult.handled; -}; - -/// Handle backspace key event when selection is not collapsed. -CommandShortcutEventHandler _backspaceInNotCollapsedSelection = (editorState) { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return KeyEventResult.ignored; - } - editorState.deleteSelectionV2(selection); - return KeyEventResult.handled; -}; - -CommandShortcutEventHandler _backspaceInBlockSelection = (editorState) { - final selection = editorState.selection; - if (selection == null || editorState.selectionType != SelectionType.block) { - return KeyEventResult.ignored; - } - final transaction = editorState.transaction; - transaction.deleteNodesAtPath(selection.start.path); - editorState - .apply(transaction) - .then((value) => editorState.selectionType = null); - - return KeyEventResult.handled; -}; - -CommandShortcutEventHandler _backspaceInSelectAll = (editorState) { - final selection = editorState.selection; - if (selection == null) { - return KeyEventResult.ignored; - } - - final transaction = editorState.transaction; - final nodes = editorState.getNodesInSelection(selection); - transaction.deleteNodes(nodes); - editorState.apply(transaction); - - return KeyEventResult.handled; -}; - -extension on EditorState { - Future deleteSelectionV2(Selection selection) async { - // Nothing to do if the selection is collapsed. - if (selection.isCollapsed) { - return false; - } - - // Normalize the selection so that it is never reversed or extended. - selection = selection.normalized; - - // Start a new transaction. - final transaction = this.transaction; - - // Get the nodes that are fully or partially selected. - final nodes = getNodesInSelection(selection); - - // If only one node is selected, then we can just delete the selected text - // or node. - if (nodes.length == 1) { - // If table cell is selected, clear the cell node child. - final node = nodes.first.type == SimpleTableCellBlockKeys.type - ? nodes.first.children.first - : nodes.first; - if (node.delta != null) { - transaction.deleteText( - node, - selection.startIndex, - selection.length, - ); - } else if (node.parent?.type != SimpleTableCellBlockKeys.type && - node.parent?.type != SimpleTableRowBlockKeys.type) { - transaction.deleteNode(node); - } - } - - // Otherwise, multiple nodes are selected, so we have to do more work. - else { - // The nodes are guaranteed to be in order, so we can determine which - // nodes are at the beginning, middle, and end of the selection. - assert(nodes.first.path < nodes.last.path); - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - - // The first node is at the beginning of the selection. - // All other nodes can be deleted. - if (i != 0) { - // Never delete a table cell node child - if (node.parent?.type == SimpleTableCellBlockKeys.type) { - if (!nodes.any((n) => n.id == node.parent?.parent?.id) && - node.delta != null) { - transaction.deleteText( - node, - 0, - min(selection.end.offset, node.delta!.length), - ); - } - } - // If first node was inside table cell then it wasn't mergable to last - // node, So we should not delete the last node. Just delete part of - // the text inside selection - else if (node.id == nodes.last.id && - nodes.first.parent?.type == SimpleTableCellBlockKeys.type) { - transaction.deleteText( - node, - 0, - selection.end.offset, - ); - } else if (node.type != SimpleTableCellBlockKeys.type && - node.type != SimpleTableRowBlockKeys.type) { - transaction.deleteNode(node); - } - continue; - } - - // If the last node is also a text node and not a node inside table cell, - // and also the current node isn't inside table cell, then we can merge - // the text between the two nodes. - if (nodes.last.delta != null && - ![node.parent?.type, nodes.last.parent?.type] - .contains(SimpleTableCellBlockKeys.type)) { - transaction.mergeText( - node, - nodes.last, - leftOffset: selection.startIndex, - rightOffset: selection.endIndex, - ); - - // combine the children of the last node into the first node. - final last = nodes.last; - - if (last.children.isNotEmpty) { - if (indentableBlockTypes.contains(node.type)) { - transaction.insertNodes( - node.path + [0], - last.children, - ); - } else { - transaction.insertNodes( - node.path.next, - last.children, - ); - } - } - } - - // Otherwise, we can just delete the selected text. - else { - // If the last or first node is inside table we will only delete - // selection part of first node. - if (nodes.last.parent?.type == SimpleTableCellBlockKeys.type || - node.parent?.type == SimpleTableCellBlockKeys.type) { - transaction.deleteText( - node, - selection.startIndex, - node.delta!.length - selection.startIndex, - ); - } else { - transaction.deleteText( - node, - selection.startIndex, - selection.length, - ); - } - } - } - } - - // After the selection is deleted, we want to move the selection to the - // beginning of the deleted selection. - transaction.afterSelection = selection.collapse(atStart: true); - - // Apply the transaction. - await apply(transaction); - - return true; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart index 86930a470bd7c..8b168bcd30160 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -38,8 +37,6 @@ List commandShortcutEvents = [ ...customTextAlignCommands, - customBackspaceCommand, - // remove standard shortcuts for copy, cut, paste, todo ...standardCommandShortcutEvents ..removeWhere(