diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart new file mode 100644 index 0000000000000..5f713a129766a --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; +import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/emoji.dart'; +import '../../shared/util.dart'; + +void main() { + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); + + testWidgets('callout with emoji icon picker', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + final emojiIconData = await tester.loadIcon(); + + /// create a new document + await tester.createNewPageWithNameUnderParent(); + + /// tap the first line of the document + await tester.editor.tapLineOfEditorAt(0); + + /// create callout + await tester.editor.showSlashMenu(); + await tester.pumpAndSettle(); + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_name_callout.tr(), + ); + + /// select an icon + final emojiPickerButton = find.descendant( + of: find.byType(CalloutBlockComponentWidget), + matching: find.byType(EmojiPickerButton), + ); + await tester.tapButton(emojiPickerButton); + await tester.tapIcon(emojiIconData); + + /// verification results + final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji)); + final iconWidget = find + .descendant( + of: emojiPickerButton, + matching: find.byType(IconWidget), + ) + .evaluate() + .first + .widget as IconWidget; + final iconWidgetData = iconWidget.data; + expect(iconWidgetData.iconContent, iconData.iconContent); + expect(iconWidgetData.iconName, iconData.iconName); + expect(iconWidgetData.groupName, iconData.groupName); + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 0f218641da6e2..27b288090ab7c 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -136,7 +136,7 @@ class _FlowyEmojiPickerState extends State { RecentIcons.getEmojiIds().then((v) { if (v.isEmpty) { emojiData = data; - setState(() => loaded = true); + if (mounted) setState(() => loaded = true); return; } final categories = List.of(data.categories); @@ -148,7 +148,7 @@ class _FlowyEmojiPickerState extends State { ), ); emojiData = EmojiData(categories: categories, emojis: data.emojis); - setState(() => loaded = true); + if (mounted) setState(() => loaded = true); }); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index 57f498484990a..fcc03d6f26768 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart' show LocaleKeys; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; @@ -33,17 +34,22 @@ class CalloutBlockKeys { /// /// The value is a String. static const String icon = 'icon'; + + /// the type of [FlowyIconType] + static const String iconType = 'icon_type'; } // The one is inserted through selection menu Node calloutNode({ Delta? delta, - String emoji = '📌', + EmojiIconData? emoji, Color? defaultColor, }) { + final defaultEmoji = emoji ?? EmojiIconData.emoji('📌'); final attributes = { CalloutBlockKeys.delta: (delta ?? Delta()).toJson(), - CalloutBlockKeys.icon: emoji, + CalloutBlockKeys.icon: defaultEmoji.emoji, + CalloutBlockKeys.iconType: defaultEmoji.type, CalloutBlockKeys.backgroundColor: defaultColor?.toHex(), }; return Node( @@ -156,12 +162,19 @@ class _CalloutBlockComponentWidgetState } // get the emoji of the note block from the node's attributes or default to '📌' - String get emoji { + EmojiIconData get emoji { final icon = node.attributes[CalloutBlockKeys.icon]; - if (icon == null || icon.isEmpty) { - return '📌'; + final type = node.attributes[CalloutBlockKeys.iconType]; + EmojiIconData result = EmojiIconData.emoji('📌'); + try { + result = EmojiIconData(FlowyIconType.values.byName(type), icon); + } catch (e) { + Log.error( + 'get emoji error with icon:[$icon], type:[$type] within alloutBlockComponentWidget', + e, + ); } - return icon; + return result; } // get access to the editor state via provider @@ -193,16 +206,16 @@ class _CalloutBlockComponentWidgetState // the emoji picker button for the note EmojiPickerButton( // force to refresh the popover state - key: ValueKey(widget.node.id + emoji), + key: ValueKey(widget.node.id + emoji.emoji), enable: editorState.editable, title: '', - emoji: EmojiIconData.emoji(emoji), + emoji: emoji, emojiSize: emojiSize, showBorder: false, buttonSize: emojiButtonSize, - onSubmitted: (emoji, controller) { - setEmoji(emoji.emoji); - controller?.close(); + onSubmitted: (r, controller) { + setEmojiIconData(r.data); + if (!r.keepOpen) controller?.close(); }, ), if (UniversalPlatform.isDesktopOrWeb) const HSpace(4.0), @@ -270,10 +283,11 @@ class _CalloutBlockComponentWidgetState } // set the emoji of the note block - Future setEmoji(String emoji) async { + Future setEmojiIconData(EmojiIconData data) async { final transaction = editorState.transaction ..updateNode(node, { - CalloutBlockKeys.icon: emoji, + CalloutBlockKeys.icon: data.emoji, + CalloutBlockKeys.iconType: data.type.name, }) ..afterSelection = Selection.collapsed( Position(path: node.path, offset: node.delta?.length ?? 0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart index dcda2941ffe88..be9063a6c8dfe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; @@ -52,7 +53,9 @@ class EditorMigration { node = pageNode(children: children); } } else if (id == 'callout') { - final emoji = nodeV0.attributes['emoji'] ?? '📌'; + final icon = nodeV0.attributes[CalloutBlockKeys.icon] ?? '📌'; + final iconType = nodeV0.attributes[CalloutBlockKeys.iconType] ?? + FlowyIconType.emoji.name; final delta = nodeV0.children.whereType().fold(Delta(), (p, e) { final delta = migrateDelta(e.delta); @@ -62,8 +65,18 @@ class EditorMigration { } return p..insert('\n'); }); + EmojiIconData? emojiIconData; + try { + emojiIconData = + EmojiIconData(FlowyIconType.values.byName(iconType), icon); + } catch (e) { + Log.error( + 'migrateNode get EmojiIconData error with :${nodeV0.attributes}', + e, + ); + } node = calloutNode( - emoji: emoji, + emoji: emojiIconData, delta: delta, ); } else if (id == 'divider') {