diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index 205725b81a..ed1295656d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -4,7 +4,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -64,7 +65,7 @@ class BlockOptionButton extends StatelessWidget { }, onSelected: (action, controller) { if (action is OptionActionWrapper) { - _onSelectAction(action.inner); + _onSelectAction(context, action.inner); controller.close(); } }, @@ -121,7 +122,7 @@ class BlockOptionButton extends StatelessWidget { ); } - void _onSelectAction(OptionAction action) { + void _onSelectAction(BuildContext context, OptionAction action) { final node = blockComponentContext.node; final transaction = editorState.transaction; switch (action) { @@ -129,10 +130,7 @@ class BlockOptionButton extends StatelessWidget { transaction.deleteNode(node); break; case OptionAction.duplicate: - transaction.insertNode( - node.path.next, - node.copyWith(), - ); + _duplicateBlock(context, transaction, node); break; case OptionAction.turnInto: break; @@ -150,4 +148,102 @@ class BlockOptionButton extends StatelessWidget { } editorState.apply(transaction); } + + void _duplicateBlock( + BuildContext context, + Transaction transaction, + Node node, + ) { + // 1. verify the node integrity + final type = node.type; + final builder = + context.read().renderer.blockComponentBuilder(type); + + if (builder == null) { + Log.error('Block type $type is not supported'); + return; + } + + final valid = builder.validate(node); + if (!valid) { + Log.error('Block type $type is not valid'); + } + + // 2. duplicate the node + // the _copyBlock will fix the table block + final newNode = _copyBlock(context, node); + + // 3. insert the node to the next of the current node + transaction.insertNode( + node.path.next, + newNode, + ); + } + + Node _copyBlock(BuildContext context, Node node) { + Node copiedNode = node.copyWith(); + + final type = node.type; + final builder = + context.read().renderer.blockComponentBuilder(type); + + if (builder == null) { + Log.error('Block type $type is not supported'); + } else { + final valid = builder.validate(node); + if (!valid) { + Log.error('Block type $type is not valid'); + if (node.type == TableBlockKeys.type) { + copiedNode = _fixTableBlock(node); + } + } + } + + return copiedNode; + } + + Node _fixTableBlock(Node node) { + if (node.type != TableBlockKeys.type) { + return node; + } + + // the table node should contains colsLen and rowsLen + final colsLen = node.attributes[TableBlockKeys.colsLen]; + final rowsLen = node.attributes[TableBlockKeys.rowsLen]; + if (colsLen == null || rowsLen == null) { + return node; + } + + final newChildren = []; + final children = node.children; + + // based on the colsLen and rowsLen, iterate the children and fix the data + for (var i = 0; i < rowsLen; i++) { + for (var j = 0; j < colsLen; j++) { + final cell = children + .where( + (n) => + n.attributes[TableCellBlockKeys.rowPosition] == i && + n.attributes[TableCellBlockKeys.colPosition] == j, + ) + .firstOrNull; + if (cell != null) { + newChildren.add(cell.copyWith()); + } else { + newChildren.add( + tableCellNode('', i, j), + ); + } + } + } + + return node.copyWith( + children: newChildren, + attributes: { + ...node.attributes, + TableBlockKeys.colsLen: colsLen, + TableBlockKeys.rowsLen: rowsLen, + }, + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index ba6f01e908..92c19db47b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -1,10 +1,11 @@ import 'dart:convert'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/startup/startup.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:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -59,25 +60,15 @@ class _ErrorBlockComponentWidgetState extends State @override Widget build(BuildContext context) { - Widget child = DecoratedBox( + Widget child = Container( + width: double.infinity, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), - child: FlowyButton( - onTap: () async { - showSnackBarMessage( - context, - LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), - ); - await getIt().setData( - ClipboardServiceData(plainText: jsonEncode(node.toJson())), - ); - }, - text: PlatformExtension.isDesktopOrWeb - ? _buildDesktopErrorBlock(context) - : _buildMobileErrorBlock(context), - ), + child: PlatformExtension.isDesktopOrWeb + ? _buildDesktopErrorBlock(context) + : _buildMobileErrorBlock(context), ); child = Padding( @@ -107,40 +98,61 @@ class _ErrorBlockComponentWidgetState extends State Widget _buildDesktopErrorBlock(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, + child: Row( children: [ - const HSpace(4), + const HSpace(12), FlowyText.regular( - LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]), ), - const HSpace(4), - FlowyText.regular( - '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', - color: Theme.of(context).hintColor, + const Spacer(), + OutlinedRoundedButton( + text: LocaleKeys.document_errorBlock_copyBlockContent.tr(), + onTap: _copyBlockContent, ), + const HSpace(12), ], ), ); } Widget _buildMobileErrorBlock(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), - ), - const VSpace(6), - FlowyText.regular( - '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', - color: Theme.of(context).hintColor, - fontSize: 12.0, - ), - ], + return AnimatedGestureDetector( + onTapUp: _copyBlockContent, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4.0, right: 24.0), + child: FlowyText.regular( + LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]), + maxLines: 3, + ), + ), + const VSpace(6), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, + fontSize: 12.0, + ), + ), + ], + ), ), ); } + + void _copyBlockContent() { + showToastNotification( + context, + message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), + ); + + getIt().setData( + ClipboardServiceData(plainText: jsonEncode(node.toJson())), + ); + } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c936af5b5b..89966137f4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: da0a56c - resolved-ref: da0a56cf721c55dc2ab944d0769d2821aba509b7 + ref: "44989c5" + resolved-ref: "44989c568e71fbf41970ec390cbb62f0db99b6e5" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "3.2.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 62b9ec4d28..3e6e8f8ab5 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -190,7 +190,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "da0a56c" + ref: "44989c5" appflowy_editor_plugins: git: diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 0a38a76617..f90bfe9b46 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1798,7 +1798,9 @@ "errorBlock": { "theBlockIsNotSupported": "Unable to parse the block content", "clickToCopyTheBlockContent": "Click to copy the block content", - "blockContentHasBeenCopied": "The block content has been copied." + "blockContentHasBeenCopied": "The block content has been copied.", + "parseError": "An error occurred while parsing the {} block.", + "copyBlockContent": "Copy block content" }, "mobilePageSelector": { "title": "Select page",