From 02eb0e0b83e8307aba0c77973db0070628927da5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:35:41 +0800 Subject: [PATCH 01/21] chore(flutter_desktop): adjust toast style (#7083) --- .../message/error_text_message.dart | 2 +- .../presentation/widgets/dialogs.dart | 159 +++++++++++++----- .../flowy_icons/16x/toast_checked_filled.svg | 3 + .../16x/{ai_close.svg => toast_close.svg} | 0 .../flowy_icons/16x/toast_error_filled.svg | 3 + .../flowy_icons/16x/toast_warning_filled.svg | 3 + .../flowy_icons/16x/warning_filled.svg | 5 - 7 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 frontend/resources/flowy_icons/16x/toast_checked_filled.svg rename frontend/resources/flowy_icons/16x/{ai_close.svg => toast_close.svg} (100%) create mode 100644 frontend/resources/flowy_icons/16x/toast_error_filled.svg create mode 100644 frontend/resources/flowy_icons/16x/toast_warning_filled.svg delete mode 100644 frontend/resources/flowy_icons/16x/warning_filled.svg diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart index 66b39fe308..6056ffffa6 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart @@ -58,7 +58,7 @@ class _ChatErrorMessageWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ const FlowySvg( - FlowySvgs.warning_filled_s, + FlowySvgs.toast_error_filled_s, blendMode: null, ), const HSpace(8.0), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 7686b1d891..42b687937e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; @@ -364,71 +365,59 @@ class OkCancelButton extends StatelessWidget { void showToastNotification( BuildContext context, { - required String message, + String? message, + TextSpan? richMessage, String? description, ToastificationType type = ToastificationType.success, ToastificationCallbacks? callbacks, double bottomPadding = 100, }) { - if (UniversalPlatform.isMobile) { - toastification.showCustom( - alignment: Alignment.bottomCenter, - autoCloseDuration: const Duration(milliseconds: 3000), - callbacks: callbacks ?? const ToastificationCallbacks(), - builder: (_, __) => _MToast( - message: message, - type: type, - bottomPadding: bottomPadding, - description: description, - ), - ); - return; - } - - toastification.show( - context: context, - type: type, - style: ToastificationStyle.flat, - closeButtonShowType: CloseButtonShowType.onHover, + assert( + (message == null) != (richMessage == null), + "Exactly one of message or richMessage must be non-null.", + ); + toastification.showCustom( alignment: Alignment.bottomCenter, autoCloseDuration: const Duration(milliseconds: 3000), - showProgressBar: false, - backgroundColor: Theme.of(context).colorScheme.surface, - borderSide: BorderSide( - color: Colors.grey.withOpacity(0.4), - ), - title: FlowyText( - message, - maxLines: 3, - ), - description: description != null - ? FlowyText.regular( - description, - fontSize: 12, - lineHeight: 1.2, - maxLines: 3, - ) - : null, + callbacks: callbacks ?? const ToastificationCallbacks(), + builder: (_, item) { + return UniversalPlatform.isMobile + ? _MobileToast( + message: message, + type: type, + bottomPadding: bottomPadding, + description: description, + ) + : _DesktopToast( + message: message, + richMessage: richMessage, + type: type, + onDismiss: () => toastification.dismiss(item), + ); + }, ); } -class _MToast extends StatelessWidget { - const _MToast({ - required this.message, +class _MobileToast extends StatelessWidget { + const _MobileToast({ + this.message, this.type = ToastificationType.success, this.bottomPadding = 100, this.description, }); - final String message; + final String? message; final ToastificationType type; final double bottomPadding; final String? description; @override Widget build(BuildContext context) { + if (message == null) { + return const SizedBox.shrink(); + } final hintText = FlowyText.regular( - message, + message!, fontSize: 16.0, figmaLineHeight: 18.0, color: Colors.white, @@ -498,6 +487,90 @@ class _MToast extends StatelessWidget { } } +class _DesktopToast extends StatelessWidget { + const _DesktopToast({ + this.message, + this.richMessage, + required this.type, + this.onDismiss, + }); + + final String? message; + final TextSpan? richMessage; + final ToastificationType type; + final void Function()? onDismiss; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 360.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + margin: const EdgeInsets.only(bottom: 32.0), + decoration: BoxDecoration( + color: Theme.of(context).isLightMode + ? const Color(0xFF333333) + : const Color(0xFF363D49), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // icon + FlowySvg( + switch (type) { + ToastificationType.warning => FlowySvgs.toast_warning_filled_s, + ToastificationType.success => FlowySvgs.toast_checked_filled_s, + ToastificationType.error => FlowySvgs.toast_error_filled_s, + _ => throw UnimplementedError(), + }, + size: const Size.square(20.0), + blendMode: null, + ), + const HSpace(8.0), + // text + Flexible( + child: message != null + ? FlowyText( + message!, + maxLines: 2, + figmaLineHeight: 20.0, + overflow: TextOverflow.ellipsis, + color: const Color(0xFFFFFFFF), + ) + : RichText( + text: richMessage!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(16.0), + // close + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onDismiss, + child: const SizedBox.square( + dimension: 24.0, + child: Center( + child: FlowySvg( + FlowySvgs.toast_close_s, + size: Size.square(16.0), + color: Color(0xFFBDBDBD), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + Future showConfirmDeletionDialog({ required BuildContext context, required String name, diff --git a/frontend/resources/flowy_icons/16x/toast_checked_filled.svg b/frontend/resources/flowy_icons/16x/toast_checked_filled.svg new file mode 100644 index 0000000000..6d43cf16c3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/toast_checked_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/ai_close.svg b/frontend/resources/flowy_icons/16x/toast_close.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/ai_close.svg rename to frontend/resources/flowy_icons/16x/toast_close.svg diff --git a/frontend/resources/flowy_icons/16x/toast_error_filled.svg b/frontend/resources/flowy_icons/16x/toast_error_filled.svg new file mode 100644 index 0000000000..bdf63223e2 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/toast_error_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/toast_warning_filled.svg b/frontend/resources/flowy_icons/16x/toast_warning_filled.svg new file mode 100644 index 0000000000..5c60f1e009 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/toast_warning_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/16x/warning_filled.svg b/frontend/resources/flowy_icons/16x/warning_filled.svg deleted file mode 100644 index b9b65cc1d7..0000000000 --- a/frontend/resources/flowy_icons/16x/warning_filled.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - From 3836545682eae7d19ebc568f097dd59bc0ae7eed Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 30 Dec 2024 16:42:14 +0800 Subject: [PATCH 02/21] fix: issues related to the emoji icon picker (#7063) * fix: remove the scrolling conflict of the icon picker on macOS * fix: the icon is not supported in sites tab * feat: keep the icon panel open after click ramdom * feat: the type of selector opened depends on the already set icon or emoji * feat: the skin tone of the random emoji follows the selected skin ton * fix: unit testing error --- .../database/database_calendar_test.dart | 10 ++- .../desktop/database/database_field_test.dart | 13 +++- .../sidebar/sidebar_view_item_test.dart | 10 ++- .../lib/plugins/base/emoji/emoji_picker.dart | 25 +++++++- .../base/emoji/emoji_picker_screen.dart | 6 +- .../presentation/database_document_title.dart | 6 +- .../base/emoji_picker_button.dart | 26 +++++--- .../cover/document_immersive_cover.dart | 5 +- .../header/document_cover_widget.dart | 24 ++++--- .../header/emoji_icon_widget.dart | 30 ++++++--- .../page_style/_page_style_icon.dart | 9 +-- .../icon_emoji_picker/emoji_search_bar.dart | 23 ++++++- .../flowy_icon_emoji_picker.dart | 64 +++++++++++++++---- .../shared/icon_emoji_picker/icon_picker.dart | 25 ++++++-- .../icon_emoji_picker/icon_search_bar.dart | 19 ++++++ .../lib/shared/icon_emoji_picker/tab.dart | 10 +++ .../lib/startup/tasks/generate_router.dart | 11 +++- .../workspace/_sidebar_workspace_icon.dart | 6 +- .../home/menu/view/view_item.dart | 12 ++-- .../menu/view/view_more_action_button.dart | 7 +- .../pages/sites/publish_info_view_item.dart | 10 ++- .../widgets/emoji_picker/emoji_menu_item.dart | 7 +- .../widgets/rename_view_popover.dart | 8 ++- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 25 files changed, 281 insertions(+), 91 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart index d6df648bb3..3a565cbee9 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('calendar', () { testWidgets('update calendar layout', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart index 1422aa8aee..6ce248a8a1 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('grid edit field test:', () { testWidgets('rename existing field', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart index 1400ccebe3..f2b721e686 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -12,7 +13,14 @@ import '../../shared/emoji.dart'; import '../../shared/util.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('Sidebar view item tests', () { testWidgets('Access view item context menu by right click', (tester) async { 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 517a0d68fa..0f218641da 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -13,6 +13,18 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; EmojiData? kCachedEmojiData; const _kRecentEmojiCategoryId = 'Recent'; +class EmojiPickerResult { + EmojiPickerResult({ + required this.emojiId, + required this.emoji, + this.isRandom = false, + }); + + final String emojiId; + final String emoji; + final bool isRandom; +} + class FlowyEmojiPicker extends StatefulWidget { const FlowyEmojiPicker({ super.key, @@ -21,7 +33,7 @@ class FlowyEmojiPicker extends StatefulWidget { this.ensureFocus = false, }); - final EmojiSelectedCallback onEmojiSelected; + final ValueChanged onEmojiSelected; final int emojiPerLine; final bool ensureFocus; @@ -70,7 +82,9 @@ class _FlowyEmojiPickerState extends State { defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, ), onEmojiSelected: (id, emoji) { - widget.onEmojiSelected.call(id, emoji); + widget.onEmojiSelected.call( + EmojiPickerResult(emojiId: id, emoji: emoji), + ); RecentIcons.putEmoji(id); }, padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -106,7 +120,12 @@ class _FlowyEmojiPickerState extends State { onSkinToneChanged: (value) { skinTone.value = value; }, - onRandomEmojiSelected: widget.onEmojiSelected, + onRandomEmojiSelected: (id, emoji) { + widget.onEmojiSelected.call( + EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true), + ); + RecentIcons.putEmoji(id); + }, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart index 513b0fd224..cf32cad611 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -11,14 +11,17 @@ class MobileEmojiPickerScreen extends StatelessWidget { const MobileEmojiPickerScreen({ super.key, this.title, + this.selectedType, this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); + final PickerTabType? selectedType; final String? title; final List tabs; static const routeName = '/emoji_picker'; static const pageTitle = 'title'; + static const iconSelectedType = 'iconSelectedType'; static const selectTabs = 'tabs'; @override @@ -30,8 +33,9 @@ class MobileEmojiPickerScreen extends StatelessWidget { body: SafeArea( child: FlowyIconEmojiPicker( tabs: tabs, + initialType: selectedType, onSelectedEmoji: (r) { - context.pop(r); + context.pop(r.data); }, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart index 04b8a30905..7c7a408b17 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -244,9 +244,9 @@ class _RenameRowPopoverState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), defaultIcon: const FlowySvg(FlowySvgs.document_s), - onSubmitted: (emoji, _) { - widget.onUpdateIcon(emoji); - PopoverContainer.of(context).close(); + onSubmitted: (r, _) { + widget.onUpdateIcon(r.data); + if (!r.keepOpen) PopoverContainer.of(context).close(); }, ), const HSpace(6), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index e2113cc1fb..894536dbcc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -26,8 +26,10 @@ class EmojiPickerButton extends StatelessWidget { final EmojiIconData emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(EmojiIconData emoji, PopoverController? controller) - onSubmitted; + final void Function( + SelectedEmojiIconResult result, + PopoverController? controller, + ) onSubmitted; final PopoverController popoverController = PopoverController(); final Widget? defaultIcon; final Offset? offset; @@ -85,8 +87,10 @@ class _DesktopEmojiPickerButton extends StatelessWidget { final EmojiIconData emoji; final double emojiSize; final Size emojiPickerSize; - final void Function(EmojiIconData emoji, PopoverController? controller) - onSubmitted; + final void Function( + SelectedEmojiIconResult result, + PopoverController? controller, + ) onSubmitted; final PopoverController popoverController = PopoverController(); final Widget? defaultIcon; final Offset? offset; @@ -113,6 +117,7 @@ class _DesktopEmojiPickerButton extends StatelessWidget { height: emojiPickerSize.height, padding: const EdgeInsets.all(4.0), child: FlowyIconEmojiPicker( + initialType: emoji.type.toPickerTabType(), onSelectedEmoji: (r) { onSubmitted(r, popoverController); }, @@ -156,8 +161,10 @@ class _MobileEmojiPickerButton extends StatelessWidget { final EmojiIconData emoji; final double emojiSize; - final void Function(EmojiIconData emoji, PopoverController? controller) - onSubmitted; + final void Function( + SelectedEmojiIconResult result, + PopoverController? controller, + ) onSubmitted; final String? title; final bool enable; final EdgeInsets? margin; @@ -177,11 +184,14 @@ class _MobileEmojiPickerButton extends StatelessWidget { final result = await context.push( Uri( path: MobileEmojiPickerScreen.routeName, - queryParameters: {MobileEmojiPickerScreen.pageTitle: title}, + queryParameters: { + MobileEmojiPickerScreen.pageTitle: title, + MobileEmojiPickerScreen.iconSelectedType: emoji.type.name, + }, ).toString(), ); if (result != null) { - onSubmitted(result, null); + onSubmitted(result.toSelectedResult(), null); } } : null, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index 05c891bd1e..7485bfe018 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -227,11 +227,12 @@ class _DocumentImmersiveCoverState extends State { value: pageStyleIconBloc, child: Expanded( child: FlowyIconEmojiPicker( + initialType: icon.type.toPickerTabType(), onSelectedEmoji: (r) { pageStyleIconBloc.add( - PageStyleIconEvent.updateIcon(r, true), + PageStyleIconEvent.updateIcon(r.data, true), ); - Navigator.pop(context); + if (!r.keepOpen) Navigator.pop(context); }, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart index 54feee3d90..d28020d0fe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart @@ -468,9 +468,9 @@ class _DocumentHeaderToolbarState extends State { popupBuilder: (BuildContext popoverContext) { isPopoverOpen = true; return FlowyIconEmojiPicker( - onSelectedEmoji: (result) { - widget.onIconOrCoverChanged(icon: result); - _popoverController.close(); + onSelectedEmoji: (r) { + widget.onIconOrCoverChanged(icon: r.data); + if (!r.keepOpen) _popoverController.close(); }, ); }, @@ -838,9 +838,7 @@ class _DocumentIconState extends State { @override Widget build(BuildContext context) { - Widget child = EmojiIconWidget( - emoji: widget.icon, - ); + Widget child = EmojiIconWidget(emoji: widget.icon); if (UniversalPlatform.isDesktopOrWeb) { child = AppFlowyPopover( @@ -852,9 +850,10 @@ class _DocumentIconState extends State { child: child, popupBuilder: (BuildContext popoverContext) { return FlowyIconEmojiPicker( - onSelectedEmoji: (result) { - widget.onChangeIcon(result); - _popoverController.close(); + initialType: widget.icon.type.toPickerTabType(), + onSelectedEmoji: (r) { + widget.onChangeIcon(r.data); + if (!r.keepOpen) _popoverController.close(); }, ); }, @@ -864,7 +863,12 @@ class _DocumentIconState extends State { child: child, onTap: () async { final result = await context.push( - MobileEmojiPickerScreen.routeName, + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: { + MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name, + }, + ).toString(), ); if (result != null) { widget.onChangeIcon(result); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index 77b0c158f5..a2f3d2aeeb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; @@ -67,24 +68,35 @@ class RawEmojiIconWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final defaultEmoji = EmojiText( - emoji: '❓', - fontSize: emojiSize, - textAlign: TextAlign.center, + final defaultEmoji = SizedBox( + width: emojiSize, + child: EmojiText( + emoji: '❓', + fontSize: emojiSize, + textAlign: TextAlign.center, + ), ); try { switch (emoji.type) { case FlowyIconType.emoji: - return EmojiText( - emoji: emoji.emoji, - fontSize: emojiSize, - textAlign: TextAlign.center, + return SizedBox( + width: emojiSize, + child: EmojiText( + emoji: emoji.emoji, + fontSize: emojiSize, + textAlign: TextAlign.center, + ), ); case FlowyIconType.icon: final iconData = IconsData.fromJson(jsonDecode(emoji.emoji)); + + /// Under the same width conditions, icons on macOS seem to appear + /// larger than emojis, so 0.9 is used here to slightly reduce the + /// size of the icons + final iconSize = Platform.isMacOS ? emojiSize * 0.9 : emojiSize; return IconWidget( data: iconData, - size: emojiSize, + size: iconSize, ); default: return defaultEmoji; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index 6b640be1c4..6647796833 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -35,7 +35,7 @@ class _PageStyleIconState extends State { builder: (context, state) { final icon = state.icon ?? EmojiIconData.none(); return GestureDetector( - onTap: () => _showIconSelector(context), + onTap: () => _showIconSelector(context, icon), behavior: HitTestBehavior.opaque, child: Container( height: 52, @@ -66,7 +66,7 @@ class _PageStyleIconState extends State { ); } - void _showIconSelector(BuildContext context) { + void _showIconSelector(BuildContext context, EmojiIconData icon) { Navigator.pop(context); final pageStyleIconBloc = PageStyleIconBloc(view: widget.view) ..add(const PageStyleIconEvent.initial()); @@ -85,11 +85,12 @@ class _PageStyleIconState extends State { value: pageStyleIconBloc, child: Expanded( child: FlowyIconEmojiPicker( + initialType: icon.type.toPickerTabType(), onSelectedEmoji: (r) { pageStyleIconBloc.add( - PageStyleIconEvent.updateIcon(r, true), + PageStyleIconEvent.updateIcon(r.data, true), ); - Navigator.pop(ctx); + if (!r.keepOpen) Navigator.pop(ctx); }, ), ), diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart index 5604da1b33..4520a2b118 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart @@ -34,6 +34,7 @@ class FlowyEmojiSearchBar extends StatefulWidget { class _FlowyEmojiSearchBarState extends State { final TextEditingController controller = TextEditingController(); + EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none; @override void dispose() { @@ -58,12 +59,18 @@ class _FlowyEmojiSearchBarState extends State { ), const HSpace(8.0), _RandomEmojiButton( + skinTone: skinTone, emojiData: widget.emojiData, onRandomEmojiSelected: widget.onRandomEmojiSelected, ), const HSpace(8.0), FlowyEmojiSkinToneSelector( - onEmojiSkinToneChanged: widget.onSkinToneChanged, + onEmojiSkinToneChanged: (v) { + setState(() { + skinTone = v; + }); + widget.onSkinToneChanged.call(v); + }, ), ], ), @@ -73,10 +80,12 @@ class _FlowyEmojiSearchBarState extends State { class _RandomEmojiButton extends StatelessWidget { const _RandomEmojiButton({ + required this.skinTone, required this.emojiData, required this.onRandomEmojiSelected, }); + final EmojiSkinTone skinTone; final EmojiData emojiData; final EmojiSelectedCallback onRandomEmojiSelected; @@ -100,9 +109,14 @@ class _RandomEmojiButton extends StatelessWidget { ), onTap: () { final random = emojiData.random; + final emojiId = random.$1; + final emoji = emojiData.getEmojiById( + emojiId, + skinTone: skinTone, + ); onRandomEmojiSelected( - random.$1, - random.$2, + emojiId, + emoji, ); }, ), @@ -131,6 +145,9 @@ class _SearchTextFieldState extends State<_SearchTextField> { @override void initState() { super.initState(); + + /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] + /// this is to ensure that focus can be regained within a short period of time if (widget.ensureFocus) { Future.delayed(const Duration(milliseconds: 200), () { if (!mounted || focusNode.hasFocus) return; diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart index 727d0b8fba..132e79e8e8 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart @@ -6,6 +6,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart' hide Icon; +import 'package:flutter/services.dart'; import 'package:universal_platform/universal_platform.dart'; extension ToProto on FlowyIconType { @@ -46,6 +47,10 @@ enum FlowyIconType { custom; } +extension FlowyIconTypeToPickerTabType on FlowyIconType { + PickerTabType? toPickerTabType() => name.toPickerTabType(); +} + class EmojiIconData { factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, ''); @@ -78,17 +83,35 @@ class EmojiIconData { bool get isNotEmpty => emoji.isNotEmpty; } +class SelectedEmojiIconResult { + SelectedEmojiIconResult(this.data, this.keepOpen); + + final EmojiIconData data; + final bool keepOpen; + + FlowyIconType get type => data.type; + + String get emoji => data.emoji; +} + +extension EmojiIconDataToSelectedResultExtension on EmojiIconData { + SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) => + SelectedEmojiIconResult(this, keepOpen); +} + class FlowyIconEmojiPicker extends StatefulWidget { const FlowyIconEmojiPicker({ super.key, this.onSelectedEmoji, + this.initialType, this.enableBackgroundColorSelection = true, this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); - final ValueChanged? onSelectedEmoji; + final ValueChanged? onSelectedEmoji; final bool enableBackgroundColorSelection; final List tabs; + final PickerTabType? initialType; @override State createState() => _FlowyIconEmojiPickerState(); @@ -96,12 +119,23 @@ class FlowyIconEmojiPicker extends StatefulWidget { class _FlowyIconEmojiPickerState extends State with SingleTickerProviderStateMixin { - late final controller = TabController( - length: widget.tabs.length, - vsync: this, - ); + late TabController controller; int currentIndex = 0; + @override + void initState() { + super.initState(); + final initialType = widget.initialType; + if (initialType != null) { + currentIndex = widget.tabs.indexOf(initialType); + } + controller = TabController( + initialIndex: currentIndex, + length: widget.tabs.length, + vsync: this, + ); + } + @override void dispose() { controller.dispose(); @@ -127,7 +161,8 @@ class _FlowyIconEmojiPickerState extends State ), _RemoveIconButton( onTap: () { - widget.onSelectedEmoji?.call(EmojiIconData.none()); + widget.onSelectedEmoji + ?.call(EmojiIconData.none().toSelectedResult()); }, ), ], @@ -155,9 +190,12 @@ class _FlowyIconEmojiPickerState extends State return FlowyEmojiPicker( ensureFocus: true, emojiPerLine: _getEmojiPerLine(context), - onEmojiSelected: (_, emoji) => widget.onSelectedEmoji?.call( - EmojiIconData.emoji(emoji), - ), + onEmojiSelected: (r) { + widget.onSelectedEmoji?.call( + EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom), + ); + SystemChannels.textInput.invokeMethod('TextInput.hide'); + }, ); } @@ -171,9 +209,13 @@ class _FlowyIconEmojiPickerState extends State Widget _buildIconPicker() { return FlowyIconPicker( + ensureFocus: true, enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: (result) { - widget.onSelectedEmoji?.call(result.toEmojiIconData()); + onSelectedIcon: (r) { + widget.onSelectedEmoji?.call( + r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom), + ); + SystemChannels.textInput.invokeMethod('TextInput.hide'); }, ); } diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart index 75f5633ee5..4e34badacd 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart @@ -75,17 +75,31 @@ Future> loadIconGroups() async { } } +class IconPickerResult { + IconPickerResult(this.data, this.isRandom); + + final IconsData data; + final bool isRandom; +} + +extension IconsDataToIconPickerResultExtension on IconsData { + IconPickerResult toResult({bool isRandom = false}) => + IconPickerResult(this, isRandom); +} + class FlowyIconPicker extends StatefulWidget { const FlowyIconPicker({ super.key, required this.onSelectedIcon, required this.enableBackgroundColorSelection, this.iconPerLine = 9, + this.ensureFocus = false, }); final bool enableBackgroundColorSelection; - final ValueChanged onSelectedIcon; + final ValueChanged onSelectedIcon; final int iconPerLine; + final bool ensureFocus; @override State createState() => _FlowyIconPickerState(); @@ -142,6 +156,7 @@ class _FlowyIconPickerState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: IconSearchBar( + ensureFocus: widget.ensureFocus, onRandomTap: () { final value = kIconGroups?.randomIcon(); if (value == null) { @@ -154,8 +169,9 @@ class _FlowyIconPickerState extends State { value.$2.content, value.$2.name, color, - ), + ).toResult(isRandom: true), ); + RecentIcons.putIcon(value.$2); }, onKeywordChanged: (keyword) => { debounce.call(() { @@ -193,14 +209,14 @@ class _FlowyIconPickerState extends State { iconGroups: filteredIconGroups, enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: widget.onSelectedIcon, + onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()), iconPerLine: widget.iconPerLine, ); } return IconPicker( iconGroups: iconGroups, enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: widget.onSelectedIcon, + onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()), iconPerLine: widget.iconPerLine, ); }, @@ -278,6 +294,7 @@ class _IconPickerState extends State { crossAxisCount: widget.iconPerLine, ), itemCount: iconGroup.icons.length, + physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { final icon = iconGroup.icons[index]; diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart index dc079bbc4e..a12be47684 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart @@ -16,9 +16,11 @@ class IconSearchBar extends StatefulWidget { super.key, required this.onRandomTap, required this.onKeywordChanged, + this.ensureFocus = false, }); final VoidCallback onRandomTap; + final bool ensureFocus; final IconKeywordChangedCallback onKeywordChanged; @override @@ -46,6 +48,7 @@ class _IconSearchBarState extends State { Expanded( child: _SearchTextField( onKeywordChanged: widget.onKeywordChanged, + ensureFocus: widget.ensureFocus, ), ), const HSpace(8.0), @@ -93,9 +96,11 @@ class _RandomIconButton extends StatelessWidget { class _SearchTextField extends StatefulWidget { const _SearchTextField({ required this.onKeywordChanged, + this.ensureFocus = false, }); final IconKeywordChangedCallback onKeywordChanged; + final bool ensureFocus; @override State<_SearchTextField> createState() => _SearchTextFieldState(); @@ -105,6 +110,20 @@ class _SearchTextFieldState extends State<_SearchTextField> { final TextEditingController controller = TextEditingController(); final FocusNode focusNode = FocusNode(); + @override + void initState() { + super.initState(); + + /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] + /// this is to ensure that focus can be regained within a short period of time + if (widget.ensureFocus) { + Future.delayed(const Duration(milliseconds: 200), () { + if (!mounted || focusNode.hasFocus) return; + focusNode.requestFocus(); + }); + } + } + @override void dispose() { controller.dispose(); diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart index 56a363132c..b74d1145c6 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart @@ -15,6 +15,16 @@ enum PickerTabType { } } +extension StringToPickerTabType on String { + PickerTabType? toPickerTabType() { + try { + return PickerTabType.values.byName(this); + } on ArgumentError { + return null; + } + } +} + class PickerTab extends StatelessWidget { const PickerTab({ super.key, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 27bfdcee5b..bcca2d0a69 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -285,14 +285,21 @@ GoRoute _mobileEmojiPickerPageRoute() { state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle]; final selectTabs = state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? ''; + final selectedType = state + .uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType] + ?.toPickerTabType(); final tabs = selectTabs .split('-') .map((e) => PickerTabType.values.byName(e)) .toList(); return MaterialExtendedPage( child: tabs.isEmpty - ? MobileEmojiPickerScreen(title: title) - : MobileEmojiPickerScreen(title: title, tabs: tabs), + ? MobileEmojiPickerScreen(title: title, selectedType: selectedType) + : MobileEmojiPickerScreen( + title: title, + selectedType: selectedType, + tabs: tabs, + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 12dec42026..1f9f4b03b8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -96,9 +96,9 @@ class _WorkspaceIconState extends State { margin: const EdgeInsets.all(0), popupBuilder: (_) => FlowyIconEmojiPicker( tabs: const [PickerTabType.emoji], - onSelectedEmoji: (result) { - widget.onSelected(result); - controller.close(); + onSelectedEmoji: (r) { + widget.onSelected(r.data); + if (!r.keepOpen) controller.close(); }, ), child: MouseRegion( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index d6b5dfe297..3f39b976d2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -641,12 +641,13 @@ class _SingleInnerViewItemState extends State { popupBuilder: (context) { isIconPickerOpened = true; return FlowyIconEmojiPicker( - onSelectedEmoji: (result) { + initialType: iconData.type.toPickerTabType(), + onSelectedEmoji: (r) { ViewBackendService.updateViewIcon( viewId: widget.view.id, - viewIcon: result, + viewIcon: r.data, ); - controller.close(); + if (!r.keepOpen) controller.close(); }, ); }, @@ -770,13 +771,12 @@ class _SingleInnerViewItemState extends State { context.read().add(const ViewEvent.collapseAllPages()); break; case ViewMoreActionType.changeIcon: - if (data is! EmojiIconData) { + if (data is! SelectedEmojiIconResult) { return; } - final result = data; await ViewBackendService.updateViewIcon( viewId: widget.view.id, - viewIcon: result, + viewIcon: data.data, ); break; case ViewMoreActionType.moveTo: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index ba9a946cc2..186c593572 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -58,7 +58,11 @@ class ViewMoreActionPopover extends StatelessWidget { (e) => ViewMoreActionTypeWrapper(e, view, (controller, data) { onEditing(false); onAction(e, data); - controller.close(); + bool enableClose = true; + if (data is SelectedEmojiIconResult) { + if (data.keepOpen) enableClose = false; + } + if (enableClose) controller.close(); }), ) .toList(); @@ -172,6 +176,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { margin: const EdgeInsets.all(0), clickHandler: PopoverClickHandler.gestureDetector, popupBuilder: (_) => FlowyIconEmojiPicker( + initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(), onSelectedEmoji: (result) => onTap(controller, result), ), child: child, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart index f2a3980bf6..3ba2c7e75e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart @@ -1,4 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -51,13 +53,9 @@ class PublishInfoViewItem extends StatelessWidget { } Widget _buildIcon() { - final icon = publishInfoView.view.icon.value; + final icon = publishInfoView.view.icon.toEmojiIconData(); return icon.isNotEmpty - ? FlowyText.emoji( - icon, - fontSize: 16.0, - figmaLineHeight: 18.0, - ) + ? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0) : publishInfoView.view.defaultIcon(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart index 6cdccb3b3b..ab952386bd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; SelectionMenuItem emojiMenuItem = SelectionMenuItem( getName: LocaleKeys.document_plugins_emoji.tr, @@ -109,7 +108,7 @@ class _EmojiSelectionMenuState extends State { @override Widget build(BuildContext context) { return FlowyEmojiPicker( - onEmojiSelected: (_, emoji) => widget.onSubmitted(emoji), + onEmojiSelected: (r) => widget.onSubmitted(r.emoji), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index ccea2f895c..cae65cafca 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -91,13 +91,15 @@ class _RenameViewPopoverState extends State { } Future _updateViewIcon( - EmojiIconData emoji, + SelectedEmojiIconResult r, PopoverController? _, ) async { await ViewBackendService.updateViewIcon( viewId: widget.viewId, - viewIcon: emoji, + viewIcon: r.data, ); - widget.popoverController.close(); + if (!r.keepOpen) { + widget.popoverController.close(); + } } } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 87a5128960..5fb55ff6f7 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -757,8 +757,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8a9fa49" - resolved-ref: "8a9fa491cb3b86baf78b0a33c2c37a29d1cae028" + ref: "355aa56" + resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8" url: "https://github.com/LucasXu0/emoji_mart.git" source: git version: "1.0.2" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 759c16e474..0fb9ce76b4 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: "8a9fa49" + ref: "355aa56" flutter_math_fork: ^0.7.3 flutter_slidable: ^3.0.0 From bdc0fa1f2a16601a640eaf833011c63e0cc10b8b Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 30 Dec 2024 16:57:59 +0800 Subject: [PATCH 03/21] fix: toolbar menu not showing beacase keyboard height is not updated in time (#7060) --- .../mobile_toolbar_v3/appflowy_mobile_toolbar.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart index cd806f6c56..787ccfda9f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart @@ -20,6 +20,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; abstract class AppFlowyMobileToolbarWidgetService { void closeItemMenu(); + void closeKeyboard(); PropertyValueNotifier get showMenuNotifier; @@ -179,7 +180,13 @@ class _MobileToolbarState extends State<_MobileToolbar> // but in this case, we don't want to update the cached keyboard height. // This is because we want to keep the same height when the menu is shown. bool canUpdateCachedKeyboardHeight = true; - ValueNotifier cachedKeyboardHeight = ValueNotifier(0.0); + + /// when the [_MobileToolbar] disposed before the keyboard height can be updated in time, + /// there will be an issue with the height being 0 + /// this is used to globally record the height. + static double _globalCachedKeyboardHeight = 0.0; + ValueNotifier cachedKeyboardHeight = + ValueNotifier(_globalCachedKeyboardHeight); // used to check if click the same item again int? selectedMenuIndex; @@ -408,6 +415,9 @@ class _MobileToolbarState extends State<_MobileToolbar> ); } } + if (keyboardHeight > 0) { + _globalCachedKeyboardHeight = keyboardHeight; + } return SizedBox( height: keyboardHeight, child: (showingMenu && selectedMenuIndex != null) From e2ee11e48ab2b3e7efd2490c8855a22dfe3ef1ff Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:17:36 +0800 Subject: [PATCH 04/21] feat: add toast messages for ai chat interactions (#7086) --- .../message/ai_message_action_bar.dart | 48 +++++++++++++++++-- .../presentation/message/ai_metadata.dart | 1 + .../presentation/message/message_util.dart | 8 ++++ frontend/resources/translations/en.json | 4 +- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart index 64183d0346..fe61398caa 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart @@ -274,7 +274,7 @@ class _SaveToPageButtonState extends State { onPressed: () async { final documentId = getOpenedDocumentId(); if (documentId != null) { - await onAddToExistingPage(documentId); + await onAddToExistingPage(context, documentId); await forceReloadAndUpdateSelection(documentId); } else { widget.onOverrideVisibility?.call(true); @@ -298,9 +298,8 @@ class _SaveToPageButtonState extends State { }, onAddToExistingPage: (documentId) async { popoverController.close(); - await onAddToExistingPage(documentId); - final view = - await ViewBackendService.getView(documentId).toNullable(); + final view = await onAddToExistingPage(context, documentId); + if (context.mounted) { openPageFromMessage(context, view); } @@ -309,12 +308,20 @@ class _SaveToPageButtonState extends State { ); } - Future onAddToExistingPage(String documentId) async { + Future onAddToExistingPage( + BuildContext context, + String documentId, + ) async { await ChatEditDocumentService.addMessageToPage( documentId, widget.textMessage, ); await Future.delayed(const Duration(milliseconds: 500)); + final view = await ViewBackendService.getView(documentId).toNullable(); + if (context.mounted) { + showSaveMessageSuccessToast(context, view); + } + return view; } void addMessageToNewPage(BuildContext context) async { @@ -327,12 +334,43 @@ class _SaveToPageButtonState extends State { chatView.parentViewId, [widget.textMessage], ); + if (context.mounted) { + showSaveMessageSuccessToast(context, newView); openPageFromMessage(context, newView); } } } + void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) { + if (view == null) { + return; + } + showToastNotification( + context, + richMessage: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.chat_addToNewPageSuccessToast.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: const Color(0xFFFFFFFF), + ), + ), + const TextSpan( + text: ' ', + ), + TextSpan( + text: view.nameOrDefault, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: const Color(0xFFFFFFFF), + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } + Future forceReloadAndUpdateSelection(String documentId) async { final bloc = DocumentBloc.findOpen(documentId); if (bloc == null) { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart index 1e06ff5f46..cc97610e8d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart @@ -90,6 +90,7 @@ class _AIMessageMetadataState extends State { data == null) { return _MetadataButton( name: m.name, + onTap: () => widget.onSelectedMetadata?.call(m), ); } return BlocProvider( diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart index 4673b9def1..97e5e84b54 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart @@ -1,8 +1,11 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -10,6 +13,11 @@ import 'package:universal_platform/universal_platform.dart'; /// on mobile void openPageFromMessage(BuildContext context, ViewPB? view) { if (view == null) { + showToastNotification( + context, + message: LocaleKeys.chat_openPagePreviewFailedToast.tr(), + type: ToastificationType.error, + ); return; } if (UniversalPlatform.isDesktop) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 284d912d6f..ef8078a2a8 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -218,7 +218,9 @@ "addToPageButton": "Add to page", "addToPageTitle": "Add message to...", "addToNewPage": "Add to a new page", - "addToNewPageName": "Messages extracted from \"{}\"" + "addToNewPageName": "Messages extracted from \"{}\"", + "addToNewPageSuccessToast": "Message added to", + "openPagePreviewFailedToast": "Failed to open page" }, "trash": { "text": "Trash", From 8e4fe3d559024b9032bb2b627c652b547b75cdfd Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 30 Dec 2024 17:53:55 +0800 Subject: [PATCH 05/21] chore: update changelog (#7095) --- CHANGELOG.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4f1e5931..188feb596f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,24 @@ # Release Notes -## Version 0.7.9 - 25/12/2024 +## Version 0.7.9 - 30/12/2024 ### New Features - +- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser. + - Create beautiful documents with 22 content types and markdown support + - Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos + - Invite members to your workspace for seamless collaboration + - Create multiple public/private spaces to better organize your content +- Simple Table is now available on Mobile, designed specifically for mobile devices. + - Create and manage Simple Table blocks on Mobile with easy-to-use action menus. + - Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile + - Use '/' to insert a content block into a table cell on Desktop +- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources +- Add messages to an editable document while chatting with AI side by side +- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons +- Drag a page from the sidebar into a document to easily mention the page without typing its title +- Paste as plain text, a new option in the right-click paste menu ### Bug Fixes +- Fixed misalignment in numbered lists +- Resolved several bugs in the emoji menu +- Fixed a bug with checklist items ## Version 0.7.8 - 18/12/2024 ### New Features From 8826e479eb3eaec533e4c26bed0a0ff3a57794f1 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:55:40 +0800 Subject: [PATCH 06/21] fix(flutter_desktop): workspace menu ui issues (#7091) * fix(flutter_desktop): remove log out and workspace option popovers conflict * test: add integration test * fix(flutter_desktop): workspace list scrollbar overlaps with list * chore(flutter_desktop): fix padding around import from notion button * chore(flutter_desktop): adjust popover conflict rules for workspace * test: add integration tests * chore(flutter_desktop): make the popoovers as barriers * fix: regression from making the workspace item menu as barrier * chore: update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart Co-authored-by: Lucas --------- Co-authored-by: Lucas --- .../collaborative_workspace_test.dart | 72 +++++++++++- .../workspace/_sidebar_workspace_actions.dart | 41 +++++-- .../workspace/_sidebar_workspace_menu.dart | 110 +++++++++--------- .../sidebar/workspace/sidebar_workspace.dart | 1 + 4 files changed, 155 insertions(+), 69 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart index 2de6fb8fa7..a473e45fb7 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart @@ -5,6 +5,7 @@ import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.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'; @@ -102,8 +103,7 @@ void main() { expect(memberCount, findsNWidgets(2)); }); - testWidgets('only display one menu item in the workspace menu', - (tester) async { + testWidgets('workspace menu popover behavior test', (tester) async { // only run the test when the feature flag is on if (!FeatureFlag.collaborativeWorkspace.isOn) { return; @@ -128,6 +128,8 @@ void main() { final workspaceItem = find.byWidgetPredicate( (w) => w is WorkspaceMenuItem && w.workspace.name == name, ); + + // the workspace menu shouldn't conflict with logout await tester.hoverOnWidget( workspaceItem, onHover: () async { @@ -136,15 +138,73 @@ void main() { ); expect(moreButton, findsOneWidget); await tester.tapButton(moreButton); + expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); + + final logoutButton = find.byType(WorkspaceMoreButton); + await tester.tapButton(logoutButton); + expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget); + expect(moreButton, findsNothing); + + await tester.tapButton(moreButton); + expect(find.text(LocaleKeys.button_logout.tr()), findsNothing); + expect(moreButton, findsOneWidget); + }, + ); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + + // clicking on the more action button for the same workspace shouldn't do + // anything + await tester.openCollaborativeWorkspaceMenu(); + await tester.hoverOnWidget( + workspaceItem, + onHover: () async { + final moreButton = find.byWidgetPredicate( + (w) => w is WorkspaceMoreActionList && w.workspace.name == name, + ); + expect(moreButton, findsOneWidget); + await tester.tapButton(moreButton); + expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); // click it again await tester.tapButton(moreButton); // nothing should happen - expect( - find.text(LocaleKeys.button_rename.tr()), - findsOneWidget, - ); + expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); + }, + ); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + + // clicking on the more button of another workspace should close the menu + // for this one + await tester.openCollaborativeWorkspaceMenu(); + final moreButton = find.byWidgetPredicate( + (w) => w is WorkspaceMoreActionList && w.workspace.name == name, + ); + await tester.hoverOnWidget( + workspaceItem, + onHover: () async { + expect(moreButton, findsOneWidget); + await tester.tapButton(moreButton); + expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); + }, + ); + + final otherWorspaceItem = find.byWidgetPredicate( + (w) => w is WorkspaceMenuItem && w.workspace.name != name, + ); + final otherMoreButton = find.byWidgetPredicate( + (w) => w is WorkspaceMoreActionList && w.workspace.name != name, + ); + await tester.hoverOnWidget( + otherWorspaceItem, + onHover: () async { + expect(otherMoreButton, findsOneWidget); + await tester.tapButton(otherMoreButton); + expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); + + expect(moreButton, findsNothing); }, ); }); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index ee5bc3fab3..44f558fc17 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -18,15 +18,23 @@ enum WorkspaceMoreAction { divider, } -class WorkspaceMoreActionList extends StatelessWidget { +class WorkspaceMoreActionList extends StatefulWidget { const WorkspaceMoreActionList({ super.key, required this.workspace, - required this.isShowingMoreActions, + required this.popoverMutex, }); final UserWorkspacePB workspace; - final ValueNotifier isShowingMoreActions; + final PopoverMutex popoverMutex; + + @override + State createState() => + _WorkspaceMoreActionListState(); +} + +class _WorkspaceMoreActionListState extends State { + bool isPopoverOpen = false; @override Widget build(BuildContext context) { @@ -45,16 +53,22 @@ class WorkspaceMoreActionList extends StatelessWidget { return PopoverActionList<_WorkspaceMoreActionWrapper>( direction: PopoverDirection.bottomWithLeftAligned, actions: actions - .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) + .map( + (action) => _WorkspaceMoreActionWrapper( + action, + widget.workspace, + () => PopoverContainer.of(context).closeAll(), + ), + ) .toList(), + mutex: widget.popoverMutex, constraints: const BoxConstraints(minWidth: 220), animationDuration: Durations.short3, slideDistance: 2, beginScaleFactor: 1.0, beginOpacity: 0.8, - onClosed: () { - isShowingMoreActions.value = false; - }, + onClosed: () => isPopoverOpen = false, + asBarrier: true, buildChild: (controller) { return SizedBox.square( dimension: 24.0, @@ -64,11 +78,10 @@ class WorkspaceMoreActionList extends StatelessWidget { FlowySvgs.workspace_three_dots_s, ), onTap: () { - if (!isShowingMoreActions.value) { + if (!isPopoverOpen) { controller.show(); + isPopoverOpen = true; } - - isShowingMoreActions.value = true; }, ), ); @@ -79,10 +92,15 @@ class WorkspaceMoreActionList extends StatelessWidget { } class _WorkspaceMoreActionWrapper extends CustomActionCell { - _WorkspaceMoreActionWrapper(this.inner, this.workspace); + _WorkspaceMoreActionWrapper( + this.inner, + this.workspace, + this.closeWorkspaceMenu, + ); final WorkspaceMoreAction inner; final UserWorkspacePB workspace; + final VoidCallback closeWorkspaceMenu; @override Widget buildWithContext( @@ -117,6 +135,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { margin: const EdgeInsets.all(6), onTap: () async { PopoverContainer.of(context).closeAll(); + closeWorkspaceMenu(); final workspaceBloc = context.read(); switch (inner) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index cfb86b8832..f3038c0bec 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -43,13 +43,7 @@ class WorkspacesMenu extends StatefulWidget { } class _WorkspacesMenuState extends State { - final ValueNotifier isShowingMoreActions = ValueNotifier(false); - - @override - void dispose() { - isShowingMoreActions.dispose(); - super.dispose(); - } + final popoverMutex = PopoverMutex(); @override Widget build(BuildContext context) { @@ -59,7 +53,7 @@ class _WorkspacesMenuState extends State { children: [ // user email Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), + padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0), child: Row( children: [ Expanded( @@ -71,18 +65,21 @@ class _WorkspacesMenuState extends State { ), ), const HSpace(4.0), - const _WorkspaceMoreButton(), + WorkspaceMoreButton( + popoverMutex: popoverMutex, + ), const HSpace(8.0), ], ), ), const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0), child: Divider(height: 1.0), ), // workspace list Flexible( child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -93,7 +90,7 @@ class _WorkspacesMenuState extends State { userProfile: widget.userProfile, isSelected: workspace.workspaceId == widget.currentWorkspace.workspaceId, - isShowingMoreActions: isShowingMoreActions, + popoverMutex: popoverMutex, ), const VSpace(6.0), ], @@ -102,13 +99,19 @@ class _WorkspacesMenuState extends State { ), ), // add new workspace - const _CreateWorkspaceButton(), - const VSpace(6.0), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6.0), + child: _CreateWorkspaceButton(), + ), if (UniversalPlatform.isDesktop) ...[ - const _ImportNotionButton(), - const VSpace(6.0), + const Padding( + padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0), + child: _ImportNotionButton(), + ), ], + + const VSpace(6.0), ], ); } @@ -132,13 +135,13 @@ class WorkspaceMenuItem extends StatefulWidget { required this.workspace, required this.userProfile, required this.isSelected, - required this.isShowingMoreActions, + required this.popoverMutex, }); final UserProfilePB userProfile; final UserWorkspacePB workspace; final bool isSelected; - final ValueNotifier isShowingMoreActions; + final PopoverMutex popoverMutex; @override State createState() => _WorkspaceMenuItemState(); @@ -230,7 +233,7 @@ class _WorkspaceMenuItemState extends State { }, child: WorkspaceMoreActionList( workspace: widget.workspace, - isShowingMoreActions: widget.isShowingMoreActions, + popoverMutex: widget.popoverMutex, ), ), const HSpace(8.0), @@ -394,40 +397,35 @@ class _ImportNotionButton extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( height: 40, - child: Stack( - alignment: Alignment.centerRight, - children: [ - FlowyButton( - key: importNotionButtonKey, - onTap: () { - _showImportNotinoDialog(context); + child: FlowyButton( + key: importNotionButtonKey, + onTap: () { + _showImportNotinoDialog(context); + }, + margin: const EdgeInsets.symmetric(horizontal: 4.0), + text: Row( + children: [ + _buildLeftIcon(context), + const HSpace(8.0), + FlowyText.regular( + LocaleKeys.workspace_importFromNotion.tr(), + ), + ], + ), + rightIcon: FlowyTooltip( + message: LocaleKeys.workspace_learnMore.tr(), + preferBelow: true, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.information_s, + ), + onPressed: () { + afLaunchUrlString( + 'https://docs.appflowy.io/docs/guides/import-from-notion', + ); }, - margin: const EdgeInsets.symmetric(horizontal: 4.0), - text: Row( - children: [ - _buildLeftIcon(context), - const HSpace(8.0), - FlowyText.regular( - LocaleKeys.workspace_importFromNotion.tr(), - ), - ], - ), ), - FlowyTooltip( - message: LocaleKeys.workspace_learnMore.tr(), - preferBelow: true, - child: FlowyIconButton( - icon: const FlowySvg( - FlowySvgs.information_s, - ), - onPressed: () { - afLaunchUrlString( - 'https://docs.appflowy.io/docs/guides/import-from-notion', - ); - }, - ), - ), - ], + ), ), ); } @@ -478,14 +476,22 @@ class _ImportNotionButton extends StatelessWidget { } } -class _WorkspaceMoreButton extends StatelessWidget { - const _WorkspaceMoreButton(); +@visibleForTesting +class WorkspaceMoreButton extends StatelessWidget { + const WorkspaceMoreButton({ + super.key, + required this.popoverMutex, + }); + + final PopoverMutex popoverMutex; @override Widget build(BuildContext context) { return AppFlowyPopover( direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 6), + mutex: popoverMutex, + asBarrier: true, popupBuilder: (_) => FlowyButton( margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0), leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index c3480a94bc..aee527d1a8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -207,6 +207,7 @@ class _SidebarSwitchWorkspaceButtonState direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 5), constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), + margin: EdgeInsets.zero, animationDuration: Durations.short3, beginScaleFactor: 1.0, beginOpacity: 0.8, From e63f7679268485d9af08564ebcc8b3d4a65fd11a Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 30 Dec 2024 17:56:43 +0800 Subject: [PATCH 07/21] feat: auto-dismiss collapsed handle on Android if no interaction occurs (#7088) * feat: support auto-dismiss collapsed handle on Android * fix: hit test area of collasepd handle is too big * chore: upgrade appflowy_editor * fix: simple table issues on mobile * feat: highlight cell after insertion * test: text color and cell background color test * fix: sign_in_page_settings_test --- .../settings/sign_in_page_settings_test.dart | 1 + .../document/presentation/editor_page.dart | 4 + .../simple_table_map_operation.dart | 241 ++++++++++++++++++ .../simple_table_more_action_popup.dart | 54 ++++ .../document/presentation/editor_style.dart | 3 +- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../simple_table_delete_operation_test.dart | 62 +++++ ...simple_table_duplicate_operation_test.dart | 64 +++++ .../simple_table_insert_operation_test.dart | 62 +++++ 10 files changed, 493 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart index b7074be357..b0b751a52f 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart @@ -67,6 +67,7 @@ void main() { // open settings page to check the result await tester.tapButton(settingsButton); + await tester.pumpAndSettle(const Duration(milliseconds: 250)); // check the server type expect( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 0cf39e8bcc..d83bd055ce 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -505,6 +505,10 @@ class _AppFlowyEditorPageState extends State Position(path: lastNode.path), ); } + + transaction.customSelectionType = SelectionType.inline; + transaction.reason = SelectionUpdateReason.uiEvent; + await editorState.apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart index cb42571704..875da5fffe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart @@ -104,6 +104,18 @@ extension TableMapOperation on Node { comparator: (iKey, index) => iKey >= index, ); + final rowBoldAttributes = _remapSource( + this.rowBoldAttributes, + index, + comparator: (iKey, index) => iKey >= index, + ); + + final rowTextColors = _remapSource( + this.rowTextColors, + index, + comparator: (iKey, index) => iKey >= index, + ); + return attributes .mergeValues( SimpleTableBlockKeys.rowColors, @@ -112,6 +124,14 @@ extension TableMapOperation on Node { .mergeValues( SimpleTableBlockKeys.rowAligns, rowAligns, + ) + .mergeValues( + SimpleTableBlockKeys.rowBoldAttributes, + rowBoldAttributes, + ) + .mergeValues( + SimpleTableBlockKeys.rowTextColors, + rowTextColors, ); } catch (e) { Log.warn('Failed to map row insertion attributes: $e'); @@ -167,6 +187,18 @@ extension TableMapOperation on Node { comparator: (iKey, index) => iKey >= index, ); + final columnBoldAttributes = _remapSource( + this.columnBoldAttributes, + index, + comparator: (iKey, index) => iKey >= index, + ); + + final columnTextColors = _remapSource( + this.columnTextColors, + index, + comparator: (iKey, index) => iKey >= index, + ); + final bool distributeColumnWidthsEvenly = attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly] ?? false; @@ -189,6 +221,14 @@ extension TableMapOperation on Node { .mergeValues( SimpleTableBlockKeys.columnWidths, columnWidths, + ) + .mergeValues( + SimpleTableBlockKeys.columnBoldAttributes, + columnBoldAttributes, + ) + .mergeValues( + SimpleTableBlockKeys.columnTextColors, + columnTextColors, ); } catch (e) { Log.warn('Failed to map row insertion attributes: $e'); @@ -238,6 +278,18 @@ extension TableMapOperation on Node { index, ); + final (rowBoldAttributes, duplicatedRowBoldAttribute) = + _findDuplicatedEntryAndRemap( + this.rowBoldAttributes, + index, + ); + + final (rowTextColors, duplicatedRowTextColor) = + _findDuplicatedEntryAndRemap( + this.rowTextColors, + index, + ); + return attributes .mergeValues( SimpleTableBlockKeys.rowColors, @@ -248,6 +300,16 @@ extension TableMapOperation on Node { SimpleTableBlockKeys.rowAligns, rowAligns, duplicatedEntry: duplicatedRowAlign, + ) + .mergeValues( + SimpleTableBlockKeys.rowBoldAttributes, + rowBoldAttributes, + duplicatedEntry: duplicatedRowBoldAttribute, + ) + .mergeValues( + SimpleTableBlockKeys.rowTextColors, + rowTextColors, + duplicatedEntry: duplicatedRowTextColor, ); } catch (e) { Log.warn('Failed to map row insertion attributes: $e'); @@ -304,6 +366,18 @@ extension TableMapOperation on Node { index, ); + final (columnBoldAttributes, duplicatedColumnBoldAttribute) = + _findDuplicatedEntryAndRemap( + this.columnBoldAttributes, + index, + ); + + final (columnTextColors, duplicatedColumnTextColor) = + _findDuplicatedEntryAndRemap( + this.columnTextColors, + index, + ); + return attributes .mergeValues( SimpleTableBlockKeys.columnColors, @@ -319,6 +393,16 @@ extension TableMapOperation on Node { SimpleTableBlockKeys.columnWidths, columnWidths, duplicatedEntry: duplicatedColumnWidth, + ) + .mergeValues( + SimpleTableBlockKeys.columnBoldAttributes, + columnBoldAttributes, + duplicatedEntry: duplicatedColumnBoldAttribute, + ) + .mergeValues( + SimpleTableBlockKeys.columnTextColors, + columnTextColors, + duplicatedEntry: duplicatedColumnTextColor, ); } catch (e) { Log.warn('Failed to map column duplication attributes: $e'); @@ -364,6 +448,7 @@ extension TableMapOperation on Node { comparator: (iKey, index) => iKey > index, filterIndex: index, ); + final columnAligns = _remapSource( this.columnAligns, index, @@ -371,6 +456,7 @@ extension TableMapOperation on Node { comparator: (iKey, index) => iKey > index, filterIndex: index, ); + final columnWidths = _remapSource( this.columnWidths, index, @@ -379,6 +465,22 @@ extension TableMapOperation on Node { filterIndex: index, ); + final columnBoldAttributes = _remapSource( + this.columnBoldAttributes, + index, + increment: false, + comparator: (iKey, index) => iKey > index, + filterIndex: index, + ); + + final columnTextColors = _remapSource( + this.columnTextColors, + index, + increment: false, + comparator: (iKey, index) => iKey > index, + filterIndex: index, + ); + return attributes .mergeValues( SimpleTableBlockKeys.columnColors, @@ -391,6 +493,14 @@ extension TableMapOperation on Node { .mergeValues( SimpleTableBlockKeys.columnWidths, columnWidths, + ) + .mergeValues( + SimpleTableBlockKeys.columnBoldAttributes, + columnBoldAttributes, + ) + .mergeValues( + SimpleTableBlockKeys.columnTextColors, + columnTextColors, ); } catch (e) { Log.warn('Failed to map column deletion attributes: $e'); @@ -443,6 +553,22 @@ extension TableMapOperation on Node { filterIndex: index, ); + final rowBoldAttributes = _remapSource( + this.rowBoldAttributes, + index, + increment: false, + comparator: (iKey, index) => iKey > index, + filterIndex: index, + ); + + final rowTextColors = _remapSource( + this.rowTextColors, + index, + increment: false, + comparator: (iKey, index) => iKey > index, + filterIndex: index, + ); + return attributes .mergeValues( SimpleTableBlockKeys.rowColors, @@ -451,6 +577,14 @@ extension TableMapOperation on Node { .mergeValues( SimpleTableBlockKeys.rowAligns, rowAligns, + ) + .mergeValues( + SimpleTableBlockKeys.rowBoldAttributes, + rowBoldAttributes, + ) + .mergeValues( + SimpleTableBlockKeys.rowTextColors, + rowTextColors, ); } catch (e) { Log.warn('Failed to map row deletion attributes: $e'); @@ -531,6 +665,10 @@ extension TableMapOperation on Node { final duplicatedColumnColor = this.columnColors[fromIndex.toString()]; final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()]; final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()]; + final duplicatedColumnBoldAttribute = + this.columnBoldAttributes[fromIndex.toString()]; + final duplicatedColumnTextColor = + this.columnTextColors[fromIndex.toString()]; /// Case 1: fromIndex > toIndex /// Before: @@ -619,6 +757,34 @@ extension TableMapOperation on Node { filterIndex: fromIndex, ); + final columnBoldAttributes = _remapSource( + this.columnBoldAttributes, + fromIndex, + increment: fromIndex > toIndex, + comparator: (iKey, index) { + if (fromIndex > toIndex) { + return iKey < fromIndex && iKey >= toIndex; + } else { + return iKey > fromIndex && iKey <= toIndex; + } + }, + filterIndex: fromIndex, + ); + + final columnTextColors = _remapSource( + this.columnTextColors, + fromIndex, + increment: fromIndex > toIndex, + comparator: (iKey, index) { + if (fromIndex > toIndex) { + return iKey < fromIndex && iKey >= toIndex; + } else { + return iKey > fromIndex && iKey <= toIndex; + } + }, + filterIndex: fromIndex, + ); + return attributes .mergeValues( SimpleTableBlockKeys.columnColors, @@ -652,6 +818,28 @@ extension TableMapOperation on Node { ) : null, removeNullValue: true, + ) + .mergeValues( + SimpleTableBlockKeys.columnBoldAttributes, + columnBoldAttributes, + duplicatedEntry: duplicatedColumnBoldAttribute != null + ? MapEntry( + toIndex.toString(), + duplicatedColumnBoldAttribute, + ) + : null, + removeNullValue: true, + ) + .mergeValues( + SimpleTableBlockKeys.columnTextColors, + columnTextColors, + duplicatedEntry: duplicatedColumnTextColor != null + ? MapEntry( + toIndex.toString(), + duplicatedColumnTextColor, + ) + : null, + removeNullValue: true, ); } catch (e) { Log.warn('Failed to map column deletion attributes: $e'); @@ -667,6 +855,9 @@ extension TableMapOperation on Node { try { final duplicatedRowColor = this.rowColors[fromIndex.toString()]; final duplicatedRowAlign = this.rowAligns[fromIndex.toString()]; + final duplicatedRowBoldAttribute = + this.rowBoldAttributes[fromIndex.toString()]; + final duplicatedRowTextColor = this.rowTextColors[fromIndex.toString()]; /// Example: /// Case 1: fromIndex > toIndex @@ -742,6 +933,34 @@ extension TableMapOperation on Node { filterIndex: fromIndex, ); + final rowBoldAttributes = _remapSource( + this.rowBoldAttributes, + fromIndex, + increment: fromIndex > toIndex, + comparator: (iKey, index) { + if (fromIndex > toIndex) { + return iKey < fromIndex && iKey >= toIndex; + } else { + return iKey > fromIndex && iKey <= toIndex; + } + }, + filterIndex: fromIndex, + ); + + final rowTextColors = _remapSource( + this.rowTextColors, + fromIndex, + increment: fromIndex > toIndex, + comparator: (iKey, index) { + if (fromIndex > toIndex) { + return iKey < fromIndex && iKey >= toIndex; + } else { + return iKey > fromIndex && iKey <= toIndex; + } + }, + filterIndex: fromIndex, + ); + return attributes .mergeValues( SimpleTableBlockKeys.rowColors, @@ -764,6 +983,28 @@ extension TableMapOperation on Node { ) : null, removeNullValue: true, + ) + .mergeValues( + SimpleTableBlockKeys.rowBoldAttributes, + rowBoldAttributes, + duplicatedEntry: duplicatedRowBoldAttribute != null + ? MapEntry( + toIndex.toString(), + duplicatedRowBoldAttribute, + ) + : null, + removeNullValue: true, + ) + .mergeValues( + SimpleTableBlockKeys.rowTextColors, + rowTextColors, + duplicatedEntry: duplicatedRowTextColor != null + ? MapEntry( + toIndex.toString(), + duplicatedRowTextColor, + ) + : null, + removeNullValue: true, ); } catch (e) { Log.warn('Failed to map row reordering attributes: $e'); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart index d2e7340790..b7b5880b38 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart @@ -455,6 +455,21 @@ class _SimpleTableMoreActionItemState extends State { final columnIndex = node.columnIndex; final editorState = context.read(); editorState.insertColumnInTable(table, columnIndex); + + final cell = table.getTableCellNode( + rowIndex: 0, + columnIndex: columnIndex, + ); + if (cell == null) { + return; + } + + // update selection + editorState.selection = Selection.collapsed( + Position( + path: cell.path.child(0), + ), + ); } void _insertColumnRight() { @@ -466,6 +481,21 @@ class _SimpleTableMoreActionItemState extends State { final columnIndex = node.columnIndex; final editorState = context.read(); editorState.insertColumnInTable(table, columnIndex + 1); + + final cell = table.getTableCellNode( + rowIndex: 0, + columnIndex: columnIndex + 1, + ); + if (cell == null) { + return; + } + + // update selection + editorState.selection = Selection.collapsed( + Position( + path: cell.path.child(0), + ), + ); } void _insertRowAbove() { @@ -477,6 +507,18 @@ class _SimpleTableMoreActionItemState extends State { final rowIndex = node.rowIndex; final editorState = context.read(); editorState.insertRowInTable(table, rowIndex); + + final cell = table.getTableCellNode(rowIndex: rowIndex, columnIndex: 0); + if (cell == null) { + return; + } + + // update selection + editorState.selection = Selection.collapsed( + Position( + path: cell.path.child(0), + ), + ); } void _insertRowBelow() { @@ -488,6 +530,18 @@ class _SimpleTableMoreActionItemState extends State { final rowIndex = node.rowIndex; final editorState = context.read(); editorState.insertRowInTable(table, rowIndex + 1); + + final cell = table.getTableCellNode(rowIndex: rowIndex + 1, columnIndex: 0); + if (cell == null) { + return; + } + + // update selection + editorState.selection = Selection.collapsed( + Position( + path: cell.path.child(0), + ), + ); } void _deleteRow() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index de4d431a08..31377a93b7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -166,9 +166,10 @@ class EditorStyleCustomizer { applyHeightToLastDescent: true, ), textSpanDecorator: customizeAttributeDecorator, - mobileDragHandleBallSize: const Size.square(12.0), magnifierSize: const Size(144, 96), textScaleFactor: textScaleFactor, + mobileDragHandleLeftExtend: 12.0, + mobileDragHandleWidthExtend: 24.0, ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 5fb55ff6f7..6e0c239fee 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: c68e5f6 - resolved-ref: c68e5f6c585205083e27e875b822656425b2853f + ref: cfb8b1b + resolved-ref: cfb8b1b6eb06f73a4fb297b6fd1d54b0ccec2922 url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 0fb9ce76b4..41047916d0 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,7 +174,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "c68e5f6" + ref: "cfb8b1b" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart index c9c1be8379..cb28f955da 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart @@ -170,5 +170,67 @@ void main() { '0': TableAlign.center.key, }); }); + + test('delete a column with text color & bold style (1)', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 3, + columnCount: 4, + ); + // delete the column 1 + final tableCellNode = + tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); + await editorState.updateColumnTextColor( + tableCellNode: tableCellNode!, + color: '0xFF0000FF', + ); + await editorState.toggleColumnBoldAttribute( + tableCellNode: tableCellNode, + isBold: true, + ); + expect(tableNode.columnTextColors, { + '1': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '1': true, + }); + await editorState.deleteColumnInTable(tableNode, 0); + expect(tableNode.columnTextColors, { + '0': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '0': true, + }); + expect(tableNode.rowLength, 3); + expect(tableNode.columnLength, 3); + }); + + test('delete a column with text color & bold style (2)', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 3, + columnCount: 4, + ); + // delete the column 1 + final tableCellNode = + tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); + await editorState.updateColumnTextColor( + tableCellNode: tableCellNode!, + color: '0xFF0000FF', + ); + await editorState.toggleColumnBoldAttribute( + tableCellNode: tableCellNode, + isBold: true, + ); + expect(tableNode.columnTextColors, { + '1': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '1': true, + }); + await editorState.deleteColumnInTable(tableNode, 1); + expect(tableNode.columnTextColors, {}); + expect(tableNode.columnBoldAttributes, {}); + expect(tableNode.rowLength, 3); + expect(tableNode.columnLength, 3); + }); }); } diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart index 123310538a..85a1c252c7 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart @@ -161,5 +161,69 @@ void main() { '1': TableAlign.center.key, }); }); + + test('duplicate a column with text color & bold style (1)', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 3, + columnCount: 4, + ); + // duplicate the column 1 + final tableCellNode = + tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); + await editorState.updateColumnTextColor( + tableCellNode: tableCellNode!, + color: '0xFF0000FF', + ); + await editorState.toggleColumnBoldAttribute( + tableCellNode: tableCellNode, + isBold: true, + ); + expect(tableNode.columnTextColors, { + '1': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '1': true, + }); + await editorState.duplicateColumnInTable(tableNode, 1); + expect(tableNode.columnTextColors, { + '1': '0xFF0000FF', + '2': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '1': true, + '2': true, + }); + }); + + test('duplicate a column with text color & bold style (2)', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 3, + columnCount: 4, + ); + // duplicate the column 1 + final tableCellNode = + tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); + await editorState.updateColumnTextColor( + tableCellNode: tableCellNode!, + color: '0xFF0000FF', + ); + await editorState.toggleColumnBoldAttribute( + tableCellNode: tableCellNode, + isBold: true, + ); + expect(tableNode.columnTextColors, { + '1': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '1': true, + }); + await editorState.duplicateColumnInTable(tableNode, 0); + expect(tableNode.columnTextColors, { + '2': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '2': true, + }); + }); }); } diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart index c84615ac42..86c4236a03 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart @@ -190,5 +190,67 @@ void main() { '0': TableAlign.center.key, }); }); + + test('insert a column with text color & bold style (1)', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 2, + columnCount: 3, + ); + // insert the column at the first position + final tableCellNode = + tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); + await editorState.updateColumnTextColor( + tableCellNode: tableCellNode!, + color: '0xFF0000FF', + ); + await editorState.toggleColumnBoldAttribute( + tableCellNode: tableCellNode, + isBold: true, + ); + expect(tableNode.columnTextColors, { + '0': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '0': true, + }); + await editorState.insertColumnInTable(tableNode, 0); + expect(tableNode.columnTextColors, { + '1': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '1': true, + }); + }); + + test('insert a column with text color & bold style (2)', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 2, + columnCount: 3, + ); + // insert the column at the first position + final tableCellNode = + tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); + await editorState.updateColumnTextColor( + tableCellNode: tableCellNode!, + color: '0xFF0000FF', + ); + await editorState.toggleColumnBoldAttribute( + tableCellNode: tableCellNode, + isBold: true, + ); + expect(tableNode.columnTextColors, { + '0': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '0': true, + }); + await editorState.insertColumnInTable(tableNode, 1); + expect(tableNode.columnTextColors, { + '0': '0xFF0000FF', + }); + expect(tableNode.columnBoldAttributes, { + '0': true, + }); + }); }); } From fca3189c97a21264eace04b4d16ea3097a13311a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 30 Dec 2024 21:28:06 +0800 Subject: [PATCH 08/21] fix: subpage block padding --- frontend/appflowy_flutter/ios/Podfile.lock | 46 +++++++++---------- .../presentation/editor_configuration.dart | 13 +++++- .../math_equation_block_component.dart | 10 ++-- .../sub_page/sub_page_block_component.dart | 7 +++ 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index ac6f338698..69bb7b20cf 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -175,37 +175,37 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 - appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 - connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 + appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a + connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 - flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 + flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 - irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 - keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a + irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 + keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 + sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 + share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 + super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 1d10c1f161..aa4756cb42 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -670,7 +670,12 @@ DatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder( ) { return DatabaseViewBlockComponentBuilder( configuration: configuration.copyWith( - padding: (_) => const EdgeInsets.symmetric(vertical: 10), + padding: (node) { + if (UniversalPlatform.isMobile) { + return configuration.padding(node); + } + return const EdgeInsets.symmetric(vertical: 10); + }, ), ); } @@ -873,6 +878,12 @@ SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder( return SubPageBlockComponentBuilder( configuration: configuration.copyWith( textStyle: (node) => styleCustomizer.subPageBlockTextStyleBuilder(), + padding: (node) { + if (UniversalPlatform.isMobile) { + return const EdgeInsets.symmetric(horizontal: 18); + } + return configuration.padding(node); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 286eac3d75..e9d6b3297e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -153,11 +153,6 @@ class MathEquationBlockComponentWidgetState ), ); - child = Padding( - padding: padding, - child: child, - ); - if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: node, @@ -174,6 +169,11 @@ class MathEquationBlockComponentWidgetState ); } + child = Padding( + padding: padding, + child: child, + ); + if (UniversalPlatform.isDesktopOrWeb) { child = Stack( children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart index 4b8550570a..c2b975ec05 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart @@ -299,6 +299,13 @@ class SubPageBlockComponentState extends State ); } + if (UniversalPlatform.isMobile) { + child = Padding( + padding: padding, + child: child, + ); + } + return child; }, ); From 60ad397105fec0c92bb8d8c4936c839f9d733855 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:19:00 +0800 Subject: [PATCH 09/21] chore: bump dependencies (#7123) * chore: bump dependencies * test: fix unit test * fix: downgrade percent indicator * chore: flutter analyze --- frontend/appflowy_flutter/ios/Podfile.lock | 44 +-- .../cell_editor/checklist_progress_bar.dart | 58 ++-- .../widgets/row/accessory/cell_accessory.dart | 7 +- frontend/appflowy_flutter/macos/Podfile.lock | 8 +- frontend/appflowy_flutter/pubspec.lock | 276 +++++++++--------- .../board_test/group_by_date_test.dart | 3 +- 6 files changed, 199 insertions(+), 197 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 69bb7b20cf..46b13d240d 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -175,36 +175,36 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 + appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 - package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart index dd831282cd..7e0b376f77 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart @@ -32,40 +32,38 @@ class _ChecklistProgressBarState extends State { return Row( children: [ Expanded( - child: Row( - children: [ - if (widget.tasks.isNotEmpty && - widget.tasks.length <= widget.segmentLimit) - ...List.generate( - widget.tasks.length, - (index) => Flexible( - child: Container( - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(2)), - color: index < numFinishedTasks - ? completedTaskColor - : AFThemeExtension.of(context).progressBarBGColor, + child: widget.tasks.isNotEmpty && + widget.tasks.length <= widget.segmentLimit + ? Row( + children: [ + ...List.generate( + widget.tasks.length, + (index) => Flexible( + child: Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(2)), + color: index < numFinishedTasks + ? completedTaskColor + : AFThemeExtension.of(context) + .progressBarBGColor, + ), + margin: const EdgeInsets.symmetric(horizontal: 1), + height: 4.0, + ), ), - margin: const EdgeInsets.symmetric(horizontal: 1), - height: 4.0, ), - ), + ], ) - else - Expanded( - child: LinearPercentIndicator( - lineHeight: 4.0, - percent: widget.percent, - padding: EdgeInsets.zero, - progressColor: completedTaskColor, - backgroundColor: - AFThemeExtension.of(context).progressBarBGColor, - barRadius: const Radius.circular(2), - ), + : LinearPercentIndicator( + lineHeight: 4.0, + percent: widget.percent, + padding: EdgeInsets.zero, + progressColor: completedTaskColor, + backgroundColor: + AFThemeExtension.of(context).progressBarBGColor, + barRadius: const Radius.circular(2), ), - ], - ), ), SizedBox( width: 45, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart index 437d125f53..6e13cc5ecb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart @@ -1,3 +1,4 @@ +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -8,7 +9,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:styled_widget/styled_widget.dart'; import '../../cell/editable_cell_builder.dart'; @@ -188,6 +188,9 @@ class CellAccessoryContainer extends StatelessWidget { ); }).toList(); - return Wrap(spacing: 6, children: children); + return SeparatedRow( + separatorBuilder: () => const HSpace(6), + children: children, + ); } } diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock index d5ac7975b9..347343dad8 100644 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ b/frontend/appflowy_flutter/macos/Podfile.lock @@ -136,24 +136,24 @@ SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 - file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d flowy_infra_ui: 03301a39ad118771adbf051a664265c61c507f38 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 HotKey: e96d8a2ddbf4591131e2bb3f54e69554d90cdca6 hotkey_manager: c32bf0bfe8f934b7bc17ab4ad5c4c142960b023c irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff - package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737 - share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 6e0c239fee..cd86f9366e 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: "direct main" description: name: any_date - sha256: "3981efcc15edd1673bcfc1aec298cc6079029fbffb3734c7eae8ceeb878f911e" + sha256: e9ed245ba44ccebf3c2d6daa3592213f409821128593d448b219a1f8e9bd17a1 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.1" app_links: dependency: "direct main" description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.3" cached_network_image: dependency: "direct main" description: @@ -374,26 +374,26 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.9.2" + version: "1.11.1" cross_cache: dependency: transitive description: name: cross_cache - sha256: ed30348320a7fefe4195c26cfcbabc76b7108ce3d364c4dd7c1b1c681a4cfe28 + sha256: "3879d1661f211e89d81ece419684df5111b5a611aa6200cd405e8332031765e9" url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.3" cross_file: dependency: "direct main" description: @@ -406,18 +406,18 @@ packages: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" dart_style: dependency: transitive description: @@ -462,10 +462,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" diff_match_patch: dependency: transitive description: @@ -550,10 +550,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" expandable: dependency: "direct main" description: @@ -566,18 +566,18 @@ packages: dependency: "direct main" description: name: extended_text_field - sha256: "954c7eea1e82728a742f7ddf09b9a51cef087d4f52b716ba88cb3eb78ccd7c6e" + sha256: fb5c35460a54906a0ada2a88a968cdfc71d71aebbaf9022debb5d67f47748964 url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.1" extended_text_library: dependency: "direct main" description: name: extended_text_library - sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" fake_async: dependency: transitive description: @@ -606,26 +606,26 @@ packages: dependency: transitive description: name: file_picker - sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204 url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.7" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -638,34 +638,34 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" fixnum: dependency: "direct main" description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e" + sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "7d97ba5c20f0e5cb1e3e2c17c865e1f797d129de31fc1f75d2dcce9470d6373c" + sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.4.1" flowy_infra: dependency: "direct main" description: @@ -703,10 +703,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.2" flutter_bloc: dependency: "direct main" description: @@ -819,18 +819,18 @@ packages: dependency: transitive description: name: flutter_shaders - sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" + sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" flutter_staggered_grid_view: dependency: "direct main" description: @@ -851,10 +851,10 @@ packages: dependency: transitive description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -877,10 +877,10 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" + sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1" url: "https://pub.dev" source: hosted - version: "8.2.8" + version: "8.2.10" freezed: dependency: "direct dev" description: @@ -930,10 +930,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" url: "https://pub.dev" source: hosted - version: "14.2.7" + version: "14.6.2" google_fonts: dependency: "direct main" description: @@ -994,10 +994,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: "direct main" description: @@ -1010,10 +1010,10 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -1058,18 +1058,18 @@ packages: dependency: transitive description: name: image_picker_for_web - sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" url: "https://pub.dev" source: hosted - version: "0.8.12" + version: "0.8.12+1" image_picker_linux: dependency: transitive description: @@ -1119,10 +1119,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" irondash_engine_context: dependency: transitive description: @@ -1255,18 +1255,18 @@ packages: dependency: transitive description: name: logger - sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" markdown: dependency: "direct main" description: @@ -1367,10 +1367,10 @@ packages: dependency: "direct main" description: name: numerus - sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837" + sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" octo_image: dependency: transitive description: @@ -1383,34 +1383,34 @@ packages: dependency: "direct main" description: name: open_filex - sha256: ba425ea49affd0a98a234aa9344b9ea5d4c4f7625a1377961eae9fe194c3d523 + sha256: dcb7bd3d32db8db5260253a62f1564c02c2c8df64bc0187cd213f65f827519bd url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.6.0" package_config: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.1.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" path: dependency: "direct main" description: @@ -1431,18 +1431,18 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: @@ -1455,10 +1455,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1519,10 +1519,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" url: "https://pub.dev" source: hosted - version: "12.0.12" + version: "12.0.13" permission_handler_apple: dependency: transitive description: @@ -1535,10 +1535,10 @@ packages: dependency: transitive description: name: permission_handler_html - sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" url: "https://pub.dev" source: hosted - version: "0.1.3+2" + version: "0.1.3+5" permission_handler_platform_interface: dependency: transitive description: @@ -1623,18 +1623,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" qr: dependency: transitive description: @@ -1735,26 +1735,26 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" url: "https://pub.dev" source: hosted - version: "10.0.2" + version: "10.1.3" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_android: dependency: transitive description: @@ -1767,10 +1767,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1885,10 +1885,10 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -1901,10 +1901,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -1957,10 +1957,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -2029,10 +2029,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1" + sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" term_glyph: dependency: transitive description: @@ -2069,26 +2069,26 @@ packages: dependency: "direct main" description: name: time - sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" toastification: dependency: "direct main" description: name: toastification - sha256: "441adf261f03b82db7067cba349756f70e9e2c0b7276bcba856210742f85f394" + sha256: "4d97fbfa463dfe83691044cba9f37cb185a79bb9205cfecb655fa1f6be126a13" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" tuple: dependency: transitive description: @@ -2141,10 +2141,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: @@ -2157,26 +2157,26 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: "direct dev" description: @@ -2197,10 +2197,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" url_protocol: dependency: "direct main" description: @@ -2214,10 +2214,10 @@ packages: dependency: "direct overridden" description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" value_layout_builder: dependency: transitive description: @@ -2230,26 +2230,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.12" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -2278,18 +2278,18 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -2366,10 +2366,10 @@ packages: dependency: transitive description: name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.1.5" window_manager: dependency: "direct main" description: @@ -2382,10 +2382,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -2398,10 +2398,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart index 92998f9cc0..51bd537159 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart @@ -109,7 +109,8 @@ void main() { assert(boardBloc.groupControllers.values.length == 2); assert( - boardBloc.boardController.groupDatas.last.headerData.groupName == "2024", + boardBloc.boardController.groupDatas.last.headerData.groupName == + DateTime.now().year.toString(), ); }); } From 0d13336b32f7d98c6503c128e692b97562400eb1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 31 Dec 2024 12:03:44 +0800 Subject: [PATCH 10/21] fix: simple tests on mobile (#7102) * fix: simple tests on mobile * fix: subpage block padding --- .../mobile/document/simple_table_test.dart | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart index fcd5494218..d6ffc5d57f 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart @@ -4,6 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -371,13 +372,22 @@ void main() { // click the column menu button await tester.clickColumnMenuButton(0); - // clear content - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_clearContents - .tr(), - ), + final clearContents = find.findTextInFlowyText( + LocaleKeys.document_plugins_simpleTable_moreActions_clearContents + .tr(), ); + + // clear content + final scrollable = find.descendant( + of: find.byType(SimpleTableBottomSheet), + matching: find.byType(Scrollable), + ); + await tester.scrollUntilVisible( + clearContents, + 100, + scrollable: scrollable, + ); + await tester.tapButton(clearContents); await tester.cancelTableActionMenu(); // check the first cell is empty @@ -427,7 +437,7 @@ void main() { // open the plus menu and select the heading block { await tester.openPlusMenuAndClickButton( - LocaleKeys.editor_toggleHeading1ShortForm.tr(), + LocaleKeys.editor_heading1.tr(), ); // check the heading block is inserted From db11886e5f975a2c25f0bb3f98559c0eda6aecee Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 31 Dec 2024 16:01:45 +0800 Subject: [PATCH 11/21] fix: simple table issues on mobile (#7115) * fix: header row/column tap areas are too small on mobile * test: header row/column tap areas are too small on mobile * feat: enable auto scroll after inserting column or row * fix: enter after emoji will create a softbreak on mobile * fix: header row/column tap areas are too small on mobile * fix: simple table alignment not work for item that wraps * test: simple table alignment not work for item that wraps --- .../mobile/document/simple_table_test.dart | 50 +++++++++- .../simple_table/simple_table_constants.dart | 3 + .../simple_table_style_operation.dart | 67 +++++++++++++ .../_desktop_simple_table_widget.dart | 8 ++ .../_mobile_simple_table_widget.dart | 8 ++ .../_simple_table_bottom_sheet_actions.dart | 96 ++++++++++++------- frontend/appflowy_flutter/pubspec.lock | 4 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../simple_table_style_operation_test.dart | 41 ++++++++ 9 files changed, 239 insertions(+), 40 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart index d6ffc5d57f..9fa8be3a9c 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -244,6 +245,53 @@ void main() { expect(table.isHeaderColumnEnabled, isTrue); expect(table.isHeaderRowEnabled, isTrue); + // disable header column + { + // focus on the first cell + unawaited( + editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: firstParagraphPath)), + reason: SelectionUpdateReason.uiEvent, + ), + ); + await tester.pumpAndSettle(); + + // click the row menu button + await tester.clickColumnMenuButton(0); + + final toggleButton = find.descendant( + of: find.byType(SimpleTableHeaderActionButton), + matching: find.byType(CupertinoSwitch), + ); + await tester.tapButton(toggleButton); + } + + // enable header row + { + // focus on the first cell + unawaited( + editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: firstParagraphPath)), + reason: SelectionUpdateReason.uiEvent, + ), + ); + await tester.pumpAndSettle(); + + // click the row menu button + await tester.clickRowMenuButton(0); + + // enable header column + final toggleButton = find.descendant( + of: find.byType(SimpleTableHeaderActionButton), + matching: find.byType(CupertinoSwitch), + ); + await tester.tapButton(toggleButton); + } + + // check the table is updated + expect(table.isHeaderColumnEnabled, isFalse); + expect(table.isHeaderRowEnabled, isFalse); + // set to page width { final table = editorState.getNodeAtPath([0])!; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart index 559b4d202d..bc6e3450e5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart @@ -86,6 +86,9 @@ class SimpleTableContext { /// This value is available on mobile only final ValueNotifier isReorderingHitIndex = ValueNotifier(null); + /// Scroll controller for the table + ScrollController? horizontalScrollController; + void _onHoveringOnColumnsAndRowsChanged() { if (!_enableTableDebugLog) { return; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart index 48ffaae3cd..59352af0ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart @@ -117,6 +117,8 @@ extension TableOptionOperation on EditorState { required Node tableCellNode, required TableAlign align, }) async { + await clearColumnTextAlign(tableCellNode: tableCellNode); + final columnIndex = tableCellNode.columnIndex; await _updateTableAttributes( tableCellNode: tableCellNode, @@ -144,6 +146,8 @@ extension TableOptionOperation on EditorState { required Node tableCellNode, required TableAlign align, }) async { + await clearRowTextAlign(tableCellNode: tableCellNode); + final rowIndex = tableCellNode.rowIndex; await _updateTableAttributes( tableCellNode: tableCellNode, @@ -385,4 +389,67 @@ extension TableOptionOperation on EditorState { transaction.updateNode(parentTableNode, attributes); await apply(transaction); } + + /// Clear the text align of the column at the index where the table cell node is located. + Future clearColumnTextAlign({ + required Node tableCellNode, + }) async { + final parentTableNode = tableCellNode.parentTableNode; + if (parentTableNode == null) { + Log.warn('parent table node is null'); + return; + } + final columnIndex = tableCellNode.columnIndex; + final transaction = this.transaction; + for (var i = 0; i < parentTableNode.rowLength; i++) { + final cell = parentTableNode.getTableCellNode( + rowIndex: i, + columnIndex: columnIndex, + ); + if (cell == null) { + continue; + } + for (final child in cell.children) { + transaction.updateNode(child, { + blockComponentAlign: null, + }); + } + } + if (transaction.operations.isNotEmpty) { + await apply(transaction); + } + } + + /// Clear the text align of the row at the index where the table cell node is located. + Future clearRowTextAlign({ + required Node tableCellNode, + }) async { + final parentTableNode = tableCellNode.parentTableNode; + if (parentTableNode == null) { + Log.warn('parent table node is null'); + return; + } + final rowIndex = tableCellNode.rowIndex; + final transaction = this.transaction; + for (var i = 0; i < parentTableNode.columnLength; i++) { + final cell = parentTableNode.getTableCellNode( + rowIndex: rowIndex, + columnIndex: i, + ); + if (cell == null) { + continue; + } + for (final child in cell.children) { + transaction.updateNode( + child, + { + blockComponentAlign: null, + }, + ); + } + } + if (transaction.operations.isNotEmpty) { + await apply(transaction); + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart index 6e640e4561..3a3f02b530 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart @@ -47,8 +47,16 @@ class _DesktopSimpleTableWidgetState extends State { final scrollController = ScrollController(); late final editorState = context.read(); + @override + void initState() { + super.initState(); + + simpleTableContext.horizontalScrollController = scrollController; + } + @override void dispose() { + simpleTableContext.horizontalScrollController = null; scrollController.dispose(); super.dispose(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart index 41ae29c61c..9b3ad2d652 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart @@ -47,8 +47,16 @@ class _MobileSimpleTableWidgetState extends State { final scrollController = ScrollController(); late final editorState = context.read(); + @override + void initState() { + super.initState(); + + simpleTableContext.horizontalScrollController = scrollController; + } + @override void dispose() { + simpleTableContext.horizontalScrollController = null; scrollController.dispose(); super.dispose(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart index 2d8b9025cf..aae1acb68b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart @@ -239,18 +239,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { SimpleTableInsertAction( type: SimpleTableMoreAction.insertAbove, enableLeftBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertAbove, + type: SimpleTableMoreAction.insertAbove, + increaseCounter: increaseCounter, ), ), const HSpace(2), SimpleTableInsertAction( type: SimpleTableMoreAction.insertBelow, enableRightBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertBelow, + type: SimpleTableMoreAction.insertBelow, + increaseCounter: increaseCounter, ), ), ], @@ -260,18 +262,20 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { SimpleTableInsertAction( type: SimpleTableMoreAction.insertLeft, enableLeftBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertLeft, + type: SimpleTableMoreAction.insertLeft, + increaseCounter: increaseCounter, ), ), const HSpace(2), SimpleTableInsertAction( type: SimpleTableMoreAction.insertRight, enableRightBorder: true, - onTap: () => _onActionTap( + onTap: (increaseCounter) async => _onActionTap( context, - SimpleTableMoreAction.insertRight, + type: SimpleTableMoreAction.insertRight, + increaseCounter: increaseCounter, ), ), ], @@ -279,7 +283,11 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { }; } - void _onActionTap(BuildContext context, SimpleTableMoreAction type) { + Future _onActionTap( + BuildContext context, { + required SimpleTableMoreAction type, + required int increaseCounter, + }) async { final simpleTableContext = context.read(); final tableNode = cellNode.parentTableNode; if (tableNode == null) { @@ -291,34 +299,48 @@ class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { case SimpleTableMoreAction.insertAbove: // update the highlight status for the selecting row simpleTableContext.selectingRow.value = cellNode.rowIndex + 1; - editorState.insertRowInTable( + await editorState.insertRowInTable( tableNode, cellNode.rowIndex, ); case SimpleTableMoreAction.insertBelow: - editorState.insertRowInTable( + await editorState.insertRowInTable( tableNode, cellNode.rowIndex + 1, ); + // scroll to the next cell position + editorState.scrollService?.scrollTo( + SimpleTableConstants.defaultRowHeight, + duration: Durations.short3, + ); case SimpleTableMoreAction.insertLeft: // update the highlight status for the selecting column simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1; - editorState.insertColumnInTable( + await editorState.insertColumnInTable( tableNode, cellNode.columnIndex, ); case SimpleTableMoreAction.insertRight: - editorState.insertColumnInTable( + await editorState.insertColumnInTable( tableNode, cellNode.columnIndex + 1, ); + final horizontalScrollController = + simpleTableContext.horizontalScrollController; + if (horizontalScrollController != null) { + final previousWidth = horizontalScrollController.offset; + horizontalScrollController.jumpTo( + previousWidth + SimpleTableConstants.defaultColumnWidth, + ); + } + default: assert(false, 'Unsupported action: $type'); } } } -class SimpleTableInsertAction extends StatelessWidget { +class SimpleTableInsertAction extends StatefulWidget { const SimpleTableInsertAction({ super.key, required this.type, @@ -330,7 +352,16 @@ class SimpleTableInsertAction extends StatelessWidget { final SimpleTableMoreAction type; final bool enableLeftBorder; final bool enableRightBorder; - final void Function() onTap; + final ValueChanged onTap; + + @override + State createState() => + _SimpleTableInsertActionState(); +} + +class _SimpleTableInsertActionState extends State { + // used to count how many times the action is tapped + int increaseCounter = 0; @override Widget build(BuildContext context) { @@ -341,19 +372,19 @@ class SimpleTableInsertAction extends StatelessWidget { shape: _buildBorder(), ), child: AnimatedGestureDetector( - onTapUp: onTap, + onTapUp: () => widget.onTap(increaseCounter++), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(1), child: FlowySvg( - type.leftIconSvg, + widget.type.leftIconSvg, size: const Size.square(22), ), ), FlowyText( - type.name, + widget.type.name, fontSize: 12, figmaLineHeight: 16, ), @@ -370,10 +401,10 @@ class SimpleTableInsertAction extends StatelessWidget { ); return RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: enableLeftBorder ? radius : Radius.zero, - bottomLeft: enableLeftBorder ? radius : Radius.zero, - topRight: enableRightBorder ? radius : Radius.zero, - bottomRight: enableRightBorder ? radius : Radius.zero, + topLeft: widget.enableLeftBorder ? radius : Radius.zero, + bottomLeft: widget.enableLeftBorder ? radius : Radius.zero, + topRight: widget.enableRightBorder ? radius : Radius.zero, + bottomRight: widget.enableRightBorder ? radius : Radius.zero, ), ); } @@ -592,7 +623,7 @@ class _SimpleTableHeaderActionButtonState child: CupertinoSwitch( value: value, activeColor: Theme.of(context).colorScheme.primary, - onChanged: (_) {}, + onChanged: (_) => _toggle(), ), ), ); @@ -1198,19 +1229,12 @@ class SimpleTableQuickActions extends StatelessWidget { SimpleTableMoreAction.copy, ), ), - FutureBuilder( - future: getIt().getData(), - builder: (context, snapshot) { - final hasContent = snapshot.data?.tableJson != null; - return SimpleTableQuickAction( - type: SimpleTableMoreAction.paste, - isEnabled: hasContent, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.paste, - ), - ); - }, + SimpleTableQuickAction( + type: SimpleTableMoreAction.paste, + onTap: () => _onActionTap( + context, + SimpleTableMoreAction.paste, + ), ), SimpleTableQuickAction( type: SimpleTableMoreAction.delete, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index cd86f9366e..3170bc8598 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: cfb8b1b - resolved-ref: cfb8b1b6eb06f73a4fb297b6fd1d54b0ccec2922 + ref: "9f6a299" + resolved-ref: "9f6a29968ecbb61678b8e0e8c9d90bcba44a24e3" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 41047916d0..e3abb161df 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,7 +174,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "cfb8b1b" + ref: "9f6a299" appflowy_editor_plugins: git: diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart index 940f03711a..dd127d3d0b 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_test/flutter_test.dart'; import 'simple_table_test_helper.dart'; @@ -193,5 +194,45 @@ void main() { expect(tableNode.tableAlign, align); } }); + + test('clear the existing align of the column before updating', () async { + final (editorState, tableNode) = createEditorStateAndTable( + rowCount: 2, + columnCount: 3, + ); + + final firstCellNode = tableNode.getTableCellNode( + rowIndex: 0, + columnIndex: 0, + ); + + Node firstParagraphNode = firstCellNode!.children.first; + + // format the first paragraph to center align + final transaction = editorState.transaction; + transaction.updateNode( + firstParagraphNode, + { + blockComponentAlign: TableAlign.right.key, + }, + ); + await editorState.apply(transaction); + + firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!; + expect( + firstParagraphNode.attributes[blockComponentAlign], + TableAlign.right.key, + ); + + await editorState.updateColumnAlign( + tableCellNode: firstCellNode, + align: TableAlign.center, + ); + + expect( + firstParagraphNode.attributes[blockComponentAlign], + null, + ); + }); }); } From 0c1eb7306a8717dada526719d39e84175096e0cf Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 3 Jan 2025 15:55:25 +0800 Subject: [PATCH 12/21] fix: convert false value in attributes to null (#7135) --- .../editor_transaction_adapter.dart | 91 ++++++++++++---- .../presentation/home/home_stack.dart | 9 +- .../editor/transaction_adapter_test.dart | 102 ++++++++++++++++++ 3 files changed, 176 insertions(+), 26 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index d66110d950..5f8d9014b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -7,20 +7,7 @@ import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_block_component.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - show - EditorState, - Transaction, - Operation, - InsertOperation, - UpdateOperation, - DeleteOperation, - PathExtensions, - Node, - Path, - Delta, - composeAttributes, - blockComponentDelta; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; @@ -287,11 +274,6 @@ extension on UpdateOperation { // create the external text if the node contains the delta in its data. final prevDelta = oldAttributes[blockComponentDelta]; final delta = attributes[blockComponentDelta]; - final diff = prevDelta != null && delta != null - ? Delta.fromJson(prevDelta).diff( - Delta.fromJson(delta), - ) - : null; final composedAttributes = composeAttributes(oldAttributes, attributes); final composedDelta = composedAttributes?[blockComponentDelta]; @@ -312,12 +294,15 @@ extension on UpdateOperation { // to be compatible with the old version, we create a new text id if the text id is empty. final textId = nanoid(6); final textDelta = composedDelta ?? delta ?? prevDelta; - final textDeltaPayloadPB = textDelta == null + final correctedTextDelta = + textDelta != null ? _correctAttributes(textDelta) : null; + + final textDeltaPayloadPB = correctedTextDelta == null ? null : TextDeltaPayloadPB( documentId: documentId, textId: textId, - delta: jsonEncode(textDelta), + delta: jsonEncode(correctedTextDelta), ); node.externalValues = ExternalValues( @@ -342,12 +327,20 @@ extension on UpdateOperation { ), ); } else { - final textDeltaPayloadPB = delta == null + final diff = prevDelta != null && delta != null + ? Delta.fromJson(prevDelta).diff( + Delta.fromJson(delta), + ) + : null; + + final correctedDiff = diff != null ? _correctDelta(diff) : null; + + final textDeltaPayloadPB = correctedDiff == null ? null : TextDeltaPayloadPB( documentId: documentId, textId: textId, - delta: jsonEncode(diff), + delta: jsonEncode(correctedDiff), ); if (enableDocumentInternalLog) { @@ -370,6 +363,58 @@ extension on UpdateOperation { return actions; } + + // if the value in Delta's attributes is false, we should set the value to null instead. + // because on Yjs, canceling format must use the null value. If we use false, the update will be rejected. + List? _correctDelta(Delta delta) { + // if the value in diff's attributes is false, we should set the value to null instead. + // because on Yjs, canceling format must use the null value. If we use false, the update will be rejected. + final correctedOps = delta.map((op) { + final attributes = op.attributes?.map( + (key, value) => MapEntry( + key, + // if the value is false, we should set the value to null instead. + value == false ? null : value, + ), + ); + + if (attributes != null) { + if (op is TextRetain) { + return TextRetain(op.length, attributes: attributes); + } else if (op is TextInsert) { + return TextInsert(op.text, attributes: attributes); + } + // ignore the other operations that do not contain attributes. + } + + return op; + }); + + return correctedOps.toList(growable: false); + } + + // Refer to [_correctDelta] for more details. + List> _correctAttributes( + List> attributes, + ) { + final correctedAttributes = attributes.map((attribute) { + return attribute.map((key, value) { + if (value is bool) { + return MapEntry(key, value == false ? null : value); + } else if (value is Map) { + return MapEntry( + key, + value.map((key, value) { + return MapEntry(key, value == false ? null : value); + }), + ); + } + return MapEntry(key, value); + }); + }).toList(growable: false); + + return correctedAttributes; + } } extension on DeleteOperation { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index d6fccee450..190c7afe5c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -2,9 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -24,6 +21,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:time/time.dart'; @@ -585,6 +584,10 @@ class PageManager { value: _notifier, child: Consumer( builder: (_, notifier, __) { + if (notifier.plugin.pluginType == PluginType.blank) { + return const BlankPage(); + } + return FadingIndexedStack( index: getIt().indexOf(notifier.plugin.pluginType), children: getIt().supportPluginTypes.map( diff --git a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart index 4a7457a43b..9e60c13ed7 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart @@ -290,5 +290,107 @@ void main() { await editorState.apply(transaction); await completer.future; }); + + test('text retain with attributes that are false', () async { + final node = paragraphNode( + delta: Delta() + ..insert( + 'Hello AppFlowy', + attributes: { + 'bold': true, + }, + ), + ); + final document = Document( + root: pageNode( + children: [ + node, + ], + ), + ); + final transactionAdapter = TransactionAdapter( + documentId: '', + documentService: DocumentService(), + ); + + final editorState = EditorState( + document: document, + ); + + int counter = 0; + final completer = Completer(); + editorState.transactionStream.listen((event) { + final time = event.$1; + if (time == TransactionTime.before) { + final actions = transactionAdapter.transactionToBlockActions( + event.$2, + editorState, + ); + final textActions = + transactionAdapter.filterTextDeltaActions(actions); + final blockActions = transactionAdapter.filterBlockActions(actions); + expect(textActions.length, 1); + expect(blockActions.length, 1); + if (counter == 1) { + // check text operation + final textAction = textActions.first; + final textId = textAction.textDeltaPayloadPB?.textId; + { + expect(textAction.textDeltaType, TextDeltaType.create); + + expect(textId, isNotEmpty); + final delta = textAction.textDeltaPayloadPB?.delta; + expect( + delta, + equals( + '[{"insert":"Hello","attributes":{"bold":null}},{"insert":" AppFlowy","attributes":{"bold":true}}]', + ), + ); + } + } else if (counter == 3) { + final textAction = textActions.first; + final textId = textAction.textDeltaPayloadPB?.textId; + { + expect(textAction.textDeltaType, TextDeltaType.update); + + expect(textId, isNotEmpty); + final delta = textAction.textDeltaPayloadPB?.delta; + expect( + delta, + equals( + '[{"retain":5,"attributes":{"bold":null}}]', + ), + ); + } + } + } else if (time == TransactionTime.after && counter == 3) { + completer.complete(); + } + }); + + counter = 1; + final insertTransaction = editorState.transaction; + insertTransaction.formatText(node, 0, 5, { + 'bold': false, + }); + + await editorState.apply(insertTransaction); + + counter = 2; + final updateTransaction = editorState.transaction; + updateTransaction.formatText(node, 0, 5, { + 'bold': true, + }); + await editorState.apply(updateTransaction); + + counter = 3; + final formatTransaction = editorState.transaction; + formatTransaction.formatText(node, 0, 5, { + 'bold': false, + }); + await editorState.apply(formatTransaction); + + await completer.future; + }); }); } From 430e21aec23ed5ea6047ecaeee9ad917bc83a284 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 5 Jan 2025 19:55:15 +0800 Subject: [PATCH 13/21] fix: simple table issues and locale issues (#7138) * fix: can't make changes on row or column of table * fix: fallback to en-US if the locale is invalid * chore: remove unused code * fix: simple table issues --- .../document_collaborators_bloc.dart | 6 ++- .../presentation/editor_configuration.dart | 44 ++++++++++++++----- .../presentation/editor_drop_handler.dart | 20 ++++++--- .../actions/drag_to_reorder/util.dart | 4 +- .../drag_to_reorder/visual_drag_area.dart | 1 + .../callout/callout_block_component.dart | 4 +- .../simple_table_more_action_popup.dart | 17 ++++--- .../sub_page/sub_page_block_component.dart | 2 + .../toggle/toggle_block_component.dart | 12 +++-- .../handlers/date_reference.dart | 40 +++++++++++++---- .../handlers/reminder_reference.dart | 37 +++++++++++++--- .../settings/date_time/date_format_ext.dart | 9 +++- .../draggable_item/draggable_item.dart | 11 +++-- frontend/appflowy_flutter/pubspec.lock | 8 ++-- frontend/appflowy_flutter/pubspec.yaml | 4 +- 15 files changed, 163 insertions(+), 56 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart index b6352b0430..74a6199b89 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart @@ -85,7 +85,11 @@ class DocumentCollaboratorsBloc final ids = {}; final sorted = states.value.values.toList() ..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - ..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId)); + // filter the duplicate users + ..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId)) + // only keep version 1 and metadata is not empty + ..retainWhere((e) => e.version == 1) + ..retainWhere((e) => e.metadata.isNotEmpty); for (final state in sorted) { if (state.version != 1) { continue; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index aa4756cb42..45763680d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -399,10 +399,11 @@ ParagraphBlockComponentBuilder _buildParagraphBlockComponentBuilder( return ParagraphBlockComponentBuilder( configuration: configuration.copyWith( placeholderText: placeholderText, - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), textAlign: (node) => _buildTextAlignInTableCell( context, @@ -421,10 +422,11 @@ TodoListBlockComponentBuilder _buildTodoListBlockComponentBuilder( return TodoListBlockComponentBuilder( configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(), - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), textAlign: (node) => _buildTextAlignInTableCell( context, @@ -451,10 +453,11 @@ BulletedListBlockComponentBuilder _buildBulletedListBlockComponentBuilder( return BulletedListBlockComponentBuilder( configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(), - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), textAlign: (node) => _buildTextAlignInTableCell( context, @@ -473,10 +476,11 @@ NumberedListBlockComponentBuilder _buildNumberedListBlockComponentBuilder( return NumberedListBlockComponentBuilder( configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(), - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), textAlign: (node) => _buildTextAlignInTableCell( context, @@ -507,10 +511,11 @@ QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder( return QuoteBlockComponentBuilder( configuration: configuration.copyWith( placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(), - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), textAlign: (node) => _buildTextAlignInTableCell( context, @@ -529,10 +534,11 @@ HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder( ) { return HeadingBlockComponentBuilder( configuration: configuration.copyWith( - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), padding: (node) { if (customHeadingPadding != null) { @@ -698,10 +704,11 @@ CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder( node: node, configuration: configuration, ), - textStyle: (node) => _buildTextStyleInTableCell( + textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ), ), inlinePadding: const EdgeInsets.symmetric(vertical: 8.0), @@ -789,11 +796,12 @@ ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder( return const EdgeInsets.only(top: 12.0, bottom: 4.0); }, - textStyle: (node) { + textStyle: (node, {TextSpan? textSpan}) { final textStyle = _buildTextStyleInTableCell( context, node: node, configuration: configuration, + textSpan: textSpan, ); final level = node.attributes[ToggleListBlockKeys.level] as int?; if (level == null) { @@ -828,9 +836,14 @@ OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder( ) { return OutlineBlockComponentBuilder( configuration: configuration.copyWith( - placeholderTextStyle: (_) => + placeholderTextStyle: (node, {TextSpan? textSpan}) => styleCustomizer.outlineBlockPlaceholderStyleBuilder(), - padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0), + padding: (node) { + if (UniversalPlatform.isMobile) { + return configuration.padding(node); + } + return const EdgeInsets.only(top: 12.0, bottom: 4.0); + }, ), ); } @@ -877,7 +890,8 @@ SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder( }) { return SubPageBlockComponentBuilder( configuration: configuration.copyWith( - textStyle: (node) => styleCustomizer.subPageBlockTextStyleBuilder(), + textStyle: (node, {TextSpan? textSpan}) => + styleCustomizer.subPageBlockTextStyleBuilder(), padding: (node) { if (UniversalPlatform.isMobile) { return const EdgeInsets.symmetric(horizontal: 18); @@ -892,8 +906,9 @@ TextStyle _buildTextStyleInTableCell( BuildContext context, { required Node node, required BlockComponentConfiguration configuration, + required TextSpan? textSpan, }) { - TextStyle textStyle = configuration.textStyle(node); + TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan); if (node.isInHeaderColumn || node.isInHeaderRow || @@ -906,6 +921,11 @@ TextStyle _buildTextStyleInTableCell( final cellTextColor = node.textColorInColumn ?? node.textColorInRow; + // enable it if we need to support the text color of the text span + // final isTextSpanColorNull = textSpan?.style?.color == null; + // final isTextSpanChildrenColorNull = + // textSpan?.children?.every((e) => e.style?.color == null) ?? true; + if (cellTextColor != null) { textStyle = textStyle.copyWith( color: buildEditorCustomizedColor( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart index 443db069d1..f39f87b75e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart @@ -1,14 +1,14 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/patterns/file_type_patterns.dart'; +import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; const _excludeFromDropTarget = [ @@ -16,6 +16,9 @@ const _excludeFromDropTarget = [ CustomImageBlockKeys.type, MultiImageBlockKeys.type, FileBlockKeys.type, + SimpleTableBlockKeys.type, + SimpleTableCellBlockKeys.type, + SimpleTableRowBlockKeys.type, ]; class EditorDropHandler extends StatelessWidget { @@ -38,8 +41,13 @@ class EditorDropHandler extends StatelessWidget { Widget build(BuildContext context) { final childWidget = Consumer( builder: (context, dropState, _) => DragTarget( - onLeave: (_) => editorState.selectionService.removeDropTarget(), + onLeave: (_) { + editorState.selectionService.removeDropTarget(); + disableAutoScrollWhenDragging = false; + }, onMove: (details) { + disableAutoScrollWhenDragging = true; + if (details.data.id == viewId) { return; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart index ddfb6f3a42..fcafe9f8de 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart @@ -140,7 +140,9 @@ bool shouldIgnoreDragTarget({ } final targetNode = editorState.getNodeAtPath(targetPath); - if (targetNode != null && targetNode.isInTable) { + if (targetNode != null && + targetNode.isInTable && + targetNode.type != SimpleTableBlockKeys.type) { return true; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart index f3499a9ea5..b4b4ffa904 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart @@ -16,6 +16,7 @@ class VisualDragArea extends StatelessWidget { final DragAreaBuilderData data; final Node dragNode; final EditorState editorState; + @override Widget build(BuildContext context) { final targetNode = data.targetNode; 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 20cf6b5902..57f4984849 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 @@ -258,10 +258,10 @@ class _CalloutBlockComponentWidgetState placeholderText: placeholderText, textAlign: alignment?.toTextAlign ?? textAlign, textSpanDecorator: (textSpan) => textSpan.updateTextStyle( - textStyle, + textStyleWithTextSpan(textSpan: textSpan), ), placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( - placeholderTextStyle, + placeholderTextStyleWithTextSpan(textSpan: textSpan), ), textDirection: textDirection, cursorColor: editorState.editorStyle.cursorColor, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart index b7b5880b38..d4df194c05 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart @@ -31,12 +31,16 @@ class _SimpleTableMoreActionPopupState RenderBox? get renderBox => context.findRenderObject() as RenderBox?; + late final simpleTableContext = context.read(); + Node? tableNode; + Node? tableCellNode; + @override void initState() { super.initState(); - final tableCellNode = - context.read().hoveringTableCell.value; + tableCellNode = context.read().hoveringTableCell.value; + tableNode = tableCellNode?.parentTableNode; gestureInterceptor = SelectionGestureInterceptor( key: 'simple_table_more_action_popup_interceptor_${tableCellNode?.id}', canTap: (details) => !_isTapInBounds(details.globalPosition), @@ -59,10 +63,6 @@ class _SimpleTableMoreActionPopupState @override Widget build(BuildContext context) { - final simpleTableContext = context.read(); - final tableCellNode = simpleTableContext.hoveringTableCell.value; - final tableNode = tableCellNode?.parentTableNode; - if (tableNode == null) { return const SizedBox.shrink(); } @@ -70,6 +70,9 @@ class _SimpleTableMoreActionPopupState return AppFlowyPopover( onOpen: () => _onOpen(tableCellNode: tableCellNode), onClose: () => _onClose(), + canClose: () async { + return true; + }, direction: widget.type == SimpleTableMoreActionType.row ? PopoverDirection.bottomWithCenterAligned : PopoverDirection.bottomWithLeftAligned, @@ -81,7 +84,7 @@ class _SimpleTableMoreActionPopupState child: SimpleTableDraggableReorderButton( editorState: editorState, simpleTableContext: simpleTableContext, - node: tableNode, + node: tableNode!, index: widget.index, isShowingMenu: widget.isShowingMenu, type: widget.type, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart index c2b975ec05..601868c887 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart @@ -200,6 +200,8 @@ class SubPageBlockComponentState extends State return const SizedBox.shrink(); } + final textStyle = textStyleWithTextSpan(); + Widget child = Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: MouseRegion( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index 9f26f7037f..78c9fe9894 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart @@ -312,7 +312,9 @@ class _ToggleListBlockComponentWidgetState placeholderText: placeholderText, lineHeight: 1.5, textSpanDecorator: (textSpan) { - var result = textSpan.updateTextStyle(textStyle); + var result = textSpan.updateTextStyle( + textStyleWithTextSpan(textSpan: textSpan), + ); if (level != null) { result = result.updateTextStyle( widget.textStyleBuilder?.call(level), @@ -321,13 +323,17 @@ class _ToggleListBlockComponentWidgetState return result; }, placeholderTextSpanDecorator: (textSpan) { - var result = textSpan.updateTextStyle(textStyle); + var result = textSpan.updateTextStyle( + textStyleWithTextSpan(textSpan: textSpan), + ); if (level != null && widget.textStyleBuilder != null) { result = result.updateTextStyle( widget.textStyleBuilder?.call(level), ); } - return result.updateTextStyle(placeholderTextStyle); + return result.updateTextStyle( + placeholderTextStyleWithTextSpan(textSpan: textSpan), + ); }, textDirection: textDirection, textAlign: alignment?.toTextAlign ?? textAlign, diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart index 904e10d362..c7076bd255 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart @@ -138,22 +138,36 @@ class DateReferenceService extends InlineActionsDelegate { final tomorrow = today.add(const Duration(days: 1)); final yesterday = today.subtract(const Duration(days: 1)); - _allOptions = [ - _itemFromDate( + late InlineActionsMenuItem todayItem; + late InlineActionsMenuItem tomorrowItem; + late InlineActionsMenuItem yesterdayItem; + + try { + todayItem = _itemFromDate( today, LocaleKeys.relativeDates_today.tr(), [DateFormat.yMd(_locale).format(today)], - ), - _itemFromDate( + ); + tomorrowItem = _itemFromDate( tomorrow, LocaleKeys.relativeDates_tomorrow.tr(), [DateFormat.yMd(_locale).format(tomorrow)], - ), - _itemFromDate( + ); + yesterdayItem = _itemFromDate( yesterday, LocaleKeys.relativeDates_yesterday.tr(), [DateFormat.yMd(_locale).format(yesterday)], - ), + ); + } catch (e) { + todayItem = _itemFromDate(today); + tomorrowItem = _itemFromDate(tomorrow); + yesterdayItem = _itemFromDate(yesterday); + } + + _allOptions = [ + todayItem, + tomorrowItem, + yesterdayItem, ]; } @@ -173,7 +187,17 @@ class DateReferenceService extends InlineActionsDelegate { String? label, List? keywords, ]) { - final labelStr = label ?? DateFormat.yMd(_locale).format(date); + late String labelStr; + if (label != null) { + labelStr = label; + } else { + try { + labelStr = DateFormat.yMd(_locale).format(date); + } catch (e) { + // fallback to en-US + labelStr = DateFormat.yMd('en-US').format(date); + } + } return InlineActionsMenuItem( label: labelStr.capitalize(), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index 372cc78698..1f479fa7c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -170,17 +170,32 @@ class ReminderReferenceService extends InlineActionsDelegate { final tomorrow = today.add(const Duration(days: 1)); final oneWeek = today.add(const Duration(days: 7)); - _allOptions = [ - _itemFromDate( + late InlineActionsMenuItem todayItem; + late InlineActionsMenuItem oneWeekItem; + + try { + todayItem = _itemFromDate( tomorrow, LocaleKeys.relativeDates_tomorrow.tr(), [DateFormat.yMd(_locale).format(tomorrow)], - ), - _itemFromDate( + ); + } catch (e) { + todayItem = _itemFromDate(today); + } + + try { + oneWeekItem = _itemFromDate( oneWeek, LocaleKeys.relativeDates_oneWeek.tr(), [DateFormat.yMd(_locale).format(oneWeek)], - ), + ); + } catch (e) { + oneWeekItem = _itemFromDate(oneWeek); + } + + _allOptions = [ + todayItem, + oneWeekItem, ]; } @@ -200,7 +215,17 @@ class ReminderReferenceService extends InlineActionsDelegate { String? label, List? keywords, ]) { - final labelStr = label ?? DateFormat.yMd(_locale).format(date); + late String labelStr; + if (label != null) { + labelStr = label; + } else { + try { + labelStr = DateFormat.yMd(_locale).format(date); + } catch (e) { + // fallback to en-US + labelStr = DateFormat.yMd('en-US').format(date); + } + } return InlineActionsMenuItem( label: labelStr.capitalize(), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart index 8d2a65c029..76fec2ecfc 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart @@ -8,7 +8,14 @@ const _friendlyFmt = 'MMM dd, y'; const _dmyFmt = 'dd/MM/y'; extension DateFormatter on UserDateFormatPB { - DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt); + DateFormat get toFormat { + try { + return DateFormat(_toFormat[this] ?? _friendlyFmt); + } catch (_) { + // fallback to en-US + return DateFormat(_toFormat[this] ?? _friendlyFmt, 'en-US'); + } + } String formatDate( DateTime date, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart index cfada71311..5b3962cd63 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; +/// This value is used to disable the auto scroll when dragging. +/// +/// It is used to prevent the auto scroll when dragging a view item to a document. +bool disableAutoScrollWhenDragging = false; + class DraggableItem extends StatefulWidget { const DraggableItem({ super.key, @@ -67,7 +72,7 @@ class _DraggableItemState extends State> { childWhenDragging: widget.childWhenDragging ?? widget.child, child: widget.child, onDragUpdate: (details) { - if (widget.enableAutoScroll) { + if (widget.enableAutoScroll && !disableAutoScrollWhenDragging) { dragTarget = details.globalPosition & widget.hitTestSize; autoScroller?.startAutoScrollIfNecessary(dragTarget!); } @@ -88,7 +93,7 @@ class _DraggableItemState extends State> { } void initAutoScrollerIfNeeded(BuildContext context) { - if (!widget.enableAutoScroll) { + if (!widget.enableAutoScroll || disableAutoScrollWhenDragging) { return; } @@ -104,7 +109,7 @@ class _DraggableItemState extends State> { autoScroller = EdgeDraggingAutoScroller( scrollable!, onScrollViewScrolled: () { - if (dragTarget != null) { + if (dragTarget != null && !disableAutoScrollWhenDragging) { autoScroller!.startAutoScrollIfNecessary(dragTarget!); } }, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 3170bc8598..5c8282724b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9f6a299" - resolved-ref: "9f6a29968ecbb61678b8e0e8c9d90bcba44a24e3" + ref: "1208cc6" + resolved-ref: "1208cc60ebaf2ef942a6d391be2aeda9621bb66b" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" @@ -70,8 +70,8 @@ packages: dependency: "direct main" description: path: "packages/appflowy_editor_plugins" - ref: "8047c21" - resolved-ref: "8047c21868273d544684522eb61e4ac2d2041409" + ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb + resolved-ref: ca8289099e40e0d6ad0605fbbe01fde3091538bb url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git" source: git version: "0.0.6" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index e3abb161df..ada9f1d9a8 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,13 +174,13 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "9f6a299" + ref: "1208cc6" appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git path: "packages/appflowy_editor_plugins" - ref: "8047c21" + ref: "ca8289099e40e0d6ad0605fbbe01fde3091538bb" sheet: git: From ace398537b42b240d5dd7336204e1f5bee50ee1f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 6 Jan 2025 17:09:27 +0800 Subject: [PATCH 14/21] fix: outline block and link preview block padding on mobile --- .../document/presentation/editor_configuration.dart | 7 ++++++- .../outline/outline_block_component.dart | 13 ++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 45763680d6..2fc2c2e7c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -854,7 +854,12 @@ LinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( ) { return LinkPreviewBlockComponentBuilder( configuration: configuration.copyWith( - padding: (_) => const EdgeInsets.symmetric(vertical: 10), + padding: (node) { + if (UniversalPlatform.isMobile) { + return configuration.padding(node); + } + return const EdgeInsets.symmetric(vertical: 10); + }, ), cache: LinkPreviewDataCache(), showMenu: true, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index 8fc3fd9145..a883c3410d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -106,10 +106,13 @@ class _OutlineBlockWidgetState extends State ); } } else { - child = MobileBlockActionButtons( - node: node, - editorState: editorState, - child: child, + child = Padding( + padding: padding, + child: MobileBlockActionButtons( + node: node, + editorState: editorState, + child: child, + ), ); } @@ -170,7 +173,7 @@ class _OutlineBlockWidgetState extends State constraints: const BoxConstraints( minHeight: 40.0, ), - padding: padding, + padding: UniversalPlatform.isMobile ? EdgeInsets.zero : padding, child: Container( padding: const EdgeInsets.symmetric( vertical: 2.0, From b3f94be33f6a76b1ae1fab56e4d602eb1a81f2e7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 6 Jan 2025 17:10:18 +0800 Subject: [PATCH 15/21] fix: filter out the space when opening new tab (#7152) * fix: filter out the space when opening tab * test: filter out the space when opening tab --- .../lib/workspace/application/home/home_bloc.dart | 8 ++++++++ .../lib/workspace/application/tabs/tabs_bloc.dart | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 2fb801595c..1afc253ab7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -1,4 +1,5 @@ import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' @@ -48,10 +49,17 @@ class HomeBloc extends Bloc { emit(state.copyWith(isLoading: e.isLoading)); }, didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { + // the latest view is shared across all the members of the workspace. + final latestView = value.setting.hasLatestView() ? value.setting.latestView : state.latestView; + if (latestView != null && latestView.isSpace) { + // If the latest view is a space, we don't need to open it. + return; + } + emit( state.copyWith( workspaceSetting: value.setting, diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart index a437c9747c..3ef76db80f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart @@ -65,6 +65,10 @@ class TabsBloc extends Bloc { state.currentPageManager.hideSecondaryPlugin(); emit(state.openPlugin(plugin: plugin, setLatest: setLatest)); if (setLatest) { + // the space view should be filtered out. + if (view != null && view.isSpace) { + return; + } _setLatestOpenView(view); } }, From 1d1647d58dcb7162d3ceedd4d680a1b72c5af1ee Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 6 Jan 2025 20:55:58 +0800 Subject: [PATCH 16/21] chore: bump version 0.8.0 --- CHANGELOG.md | 16 ++++++++++++++++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188feb596f..2007cce687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,20 @@ # Release Notes +## Version 0.8.0 - 06/01/2025 +### Bug Fixes +Fixed error displaying in the page style menu +Fixed filter logic in the icon picker +Fixed error displaying in the Favorite/Recent page +Fixed the color picker displaying when tapping down +Fixed icons not being supported in subpage blocks +Fixed recent icon functionality in the space icon menu +Fixed "Insert Below" not auto-scrolling the table +Fixed a to-do item with an emoji automatically creating a soft break +Fixed header row/column tap areas being too small +Fixed simple table alignment not working for items that wrap +Fixed web content reverting after removing the inline code format on desktop +Fixed inability to make changes to a row or column in the table when opening a new tab +Fixed changing the language to CKB-KU causing a gray screen on mobile + ## Version 0.7.9 - 30/12/2024 ### New Features - Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser. diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index bcf4baaeea..4053570e13 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.7.9" +APPFLOWY_VERSION = "0.8.0" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index ada9f1d9a8..a29396b15c 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -4,7 +4,7 @@ description: Bring projects, wikis, and teams together with AI. AppFlowy is an your data. The best open source alternative to Notion. publish_to: "none" -version: 0.7.9 +version: 0.8.0 environment: flutter: ">=3.22.0" From 776f70a0c7e325e5daae605288e060eb3f07d62c Mon Sep 17 00:00:00 2001 From: Morn Date: Mon, 6 Jan 2025 20:04:49 +0800 Subject: [PATCH 17/21] fix: icon picker issues on mobile (#7114) * fix: icon picker issues on mobile (#7113) * fix: error displaying in Page style * fix: error displaying in Favorite/Recent page * fix: complete the filter logic of icon picker * fix: the color picker showed when tapping down * fix: icons are not supported in subpage blocks * chore: add some tests * fix: recent icons not working for grid header icon * fix: recent icon doesn't work in space icon (#7133) --------- Co-authored-by: Lucas.Xu --- .../desktop/cloud/cloud_runner.dart | 2 + .../cloud/sidebar/sidebar_icon_test.dart | 62 +++++++++++++++++++ .../document/document_sub_page_test.dart | 44 ++++++++++++- .../desktop/sidebar/sidebar_icon_test.dart | 21 +------ .../mobile/document/page_style_test.dart | 47 +++++++++++++- .../integration_test/shared/base.dart | 27 ++++++++ .../shared/common_operations.dart | 18 ++++++ .../integration_test/shared/emoji.dart | 10 +-- .../presentation/base/mobile_view_page.dart | 4 +- .../home/mobile_home_page_header.dart | 2 + .../home/shared/mobile_page_card.dart | 18 +++--- .../header/emoji_icon_widget.dart | 11 ++-- .../page_style/_page_style_icon.dart | 15 +++-- .../sub_page/sub_page_block_component.dart | 12 ++-- .../lib/shared/icon_emoji_picker/icon.dart | 25 +++++++- .../shared/icon_emoji_picker/icon_picker.dart | 47 +++++++++++--- .../icon_emoji_picker/recent_icons.dart | 21 ++++--- .../lib/startup/tasks/generate_router.dart | 14 +++-- .../sidebar/space/sidebar_space_header.dart | 31 +++++----- .../unit_test/util/recent_icons_test.dart | 9 +-- 20 files changed, 344 insertions(+), 96 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart index 0b35cffe51..bc994eba99 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart @@ -2,6 +2,7 @@ import 'data_migration/data_migration_test_runner.dart' as data_migration_test_runner; import 'document/document_test_runner.dart' as document_test_runner; import 'set_env.dart' as preset_af_cloud_env_test; +import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test; import 'sidebar/sidebar_rename_untitled_test.dart' as sidebar_rename_untitled_test; @@ -26,4 +27,5 @@ Future main() async { // sidebar sidebar_move_page_test.main(); sidebar_rename_untitled_test.main(); + sidebar_icon_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart new file mode 100644 index 0000000000..d6688ef07f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart'; +import 'package:flowy_svg/flowy_svg.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('Change slide bar space icon', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + final emojiIconData = await tester.loadIcon(); + final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji)); + + await tester.hoverOnWidget( + find.byType(SidebarSpaceHeader), + onHover: () async { + final moreOption = find.byType(SpaceMorePopup); + await tester.tapButton(moreOption); + expect(find.byType(FlowyIconEmojiPicker), findsNothing); + await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg); + expect(find.byType(FlowyIconEmojiPicker), findsOneWidget); + }, + ); + + final icons = find.byWidgetPredicate( + (w) => w is FlowySvg && w.svgString == firstIcon.iconContent, + ); + expect(icons, findsOneWidget); + await tester.tapIcon(EmojiIconData.icon(firstIcon)); + + final spaceHeader = find.byType(SidebarSpaceHeader); + final spaceIcon = find.descendant( + of: spaceHeader, + matching: find.byWidgetPredicate( + (w) => w is FlowySvg && w.svgString == firstIcon.iconContent, + ), + ); + expect(spaceIcon, findsOneWidget); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart index e6bdf9e6b6..d4d6cbab02 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart @@ -1,7 +1,9 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -11,6 +13,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import '../../shared/emoji.dart'; import '../../shared/util.dart'; // Test cases for the Document SubPageBlock that needs to be covered: @@ -37,7 +40,14 @@ import '../../shared/util.dart'; const _defaultPageName = ""; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('Document SubPageBlock tests', () { testWidgets('Insert a new SubPageBlock from Slash menu items', @@ -498,6 +508,38 @@ void main() { expect(find.text('Parent'), findsNWidgets(2)); }); + + testWidgets('Displaying icon of subpage', (tester) async { + const firstPage = 'FirstPage'; + + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.createNewPageWithNameUnderParent(name: firstPage); + final icon = await tester.loadIcon(); + + /// create subpage + await tester.editor.tapLineOfEditorAt(0); + await tester.editor.showSlashMenu(); + await tester.editor.tapSlashMenuItemWithName( + LocaleKeys.document_slashMenu_subPage_name.tr(), + offset: 100, + ); + + /// add icon + await tester.editor.hoverOnCoverToolbar(); + await tester.editor.tapAddIconButton(); + await tester.tapIcon(icon); + await tester.pumpAndSettle(); + await tester.openPage(firstPage); + + /// check if there is a icon in document + final iconWidget = find.byWidgetPredicate((w) { + if (w is! RawEmojiIconWidget) return false; + final iconData = w.emoji.emoji; + return iconData == icon.emoji; + }); + expect(iconWidget, findsOneWidget); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart index 7c7b9e1f1c..7b2925b5b2 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart @@ -1,9 +1,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; @@ -27,21 +25,6 @@ void main() { RecentIcons.enable = true; }); - Future loadIcon() async { - await loadIconGroups(); - final groups = kIconGroups!; - final firstGroup = groups.first; - final firstIcon = firstGroup.icons.first; - return EmojiIconData.icon( - IconsData( - firstGroup.name, - firstIcon.content, - firstIcon.name, - builtInSpaceColors.first, - ), - ); - } - testWidgets('Update page emoji in sidebar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -160,7 +143,7 @@ void main() { testWidgets('Update page icon in sidebar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - final iconData = await loadIcon(); + final iconData = await tester.loadIcon(); // create document, board, grid and calendar views for (final value in ViewLayoutPB.values) { @@ -192,7 +175,7 @@ void main() { testWidgets('Update page icon in title bar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - final iconData = await loadIcon(); + final iconData = await tester.loadIcon(); // create document, board, grid and calendar views for (final value in ViewLayoutPB.values) { diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart index 4d34861850..e3d3bc093f 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart @@ -1,17 +1,30 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; +import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart'; +import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.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() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + RecentIcons.enable = false; + }); + + tearDownAll(() { + RecentIcons.enable = true; + }); group('document page style:', () { double getCurrentEditorFontSize() { @@ -114,5 +127,37 @@ void main() { ); expect(builtInCover, findsOneWidget); }); + + testWidgets('page style icon', (tester) async { + await tester.launchInAnonymousMode(); + + final createPageButton = + find.byKey(BottomNavigationBarItemType.add.valueKey); + await tester.tapButton(createPageButton); + + /// toggle the preset button + await tester.tapSvgButton(FlowySvgs.m_layout_s); + + /// select document plugins emoji + final pageStyleIcon = find.byType(PageStyleIcon); + + /// there should be none of emoji + final noneText = find.text(LocaleKeys.pageStyle_none.tr()); + expect(noneText, findsOneWidget); + await tester.tapButton(pageStyleIcon); + + /// select an emoji + const emoji = '😄'; + await tester.tapEmoji(emoji); + await tester.tapSvgButton(FlowySvgs.m_layout_s); + expect(noneText, findsNothing); + expect( + find.descendant( + of: pageStyleIcon, + matching: find.text(emoji), + ), + findsOneWidget, + ); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart index f6baa52721..4a7ec9081f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester { } } + Future tapDown( + Finder finder, { + int? pointer, + int buttons = kPrimaryButton, + PointerDeviceKind kind = PointerDeviceKind.touch, + bool pumpAndSettle = true, + int milliseconds = 500, + }) async { + final location = getCenter(finder); + final TestGesture gesture = await startGesture( + location, + pointer: pointer, + buttons: buttons, + kind: kind, + ); + await gesture.cancel(); + await gesture.down(location); + await gesture.cancel(); + if (pumpAndSettle) { + await this.pumpAndSettle( + Duration(milliseconds: milliseconds), + EnginePhase.sendSemanticsUpdate, + const Duration(seconds: 15), + ); + } + } + Future tapButtonWithName( String tr, { int milliseconds = 500, diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 6ac67d0e33..7ab545318b 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -13,6 +13,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab import 'package:appflowy/plugins/shared/share/share_button.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; @@ -23,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; @@ -898,6 +900,22 @@ extension CommonOperations on WidgetTester { await tapAt(Offset.zero); await pumpUntilNotFound(finder); } + + /// load icon list and return the first one + Future loadIcon() async { + await loadIconGroups(); + final groups = kIconGroups!; + final firstGroup = groups.first; + final firstIcon = firstGroup.icons.first; + return EmojiIconData.icon( + IconsData( + firstGroup.name, + firstIcon.content, + firstIcon.name, + builtInSpaceColors.first, + ), + ); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/integration_test/shared/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart index e6c756edb1..c2032eef6b 100644 --- a/frontend/appflowy_flutter/integration_test/shared/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/shared/emoji.dart @@ -31,10 +31,7 @@ extension EmojiTestExtension on WidgetTester { matching: find.text(PickerTabType.icon.tr), ); expect(iconTab, findsOneWidget); - expect(find.byType(FlowyIconPicker), findsNothing); - await tap(iconTab); - await pumpAndSettle(); - expect(find.byType(FlowyIconPicker), findsOneWidget); + await tapButton(iconTab); final selectedSvg = find.descendant( of: find.byType(FlowyIconPicker), matching: find.byWidgetPredicate( @@ -42,6 +39,11 @@ extension EmojiTestExtension on WidgetTester { ), ); expect(find.byType(IconColorPicker), findsNothing); + + /// test for tapping down, it should not display the ColorPicker unless tapping up + await tapDown(selectedSvg); + expect(find.byType(IconColorPicker), findsNothing); + await tapButton(selectedSvg); final colorPicker = find.byType(IconColorPicker); expect(colorPicker, findsOneWidget); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 6e0a77169b..4fd8e246d7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -268,8 +268,8 @@ class _MobileViewPageState extends State { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (icon != null) ...[ - EmojiIconWidget( + if (icon != null && icon.value.isNotEmpty) ...[ + RawEmojiIconWidget( emoji: icon.toEmojiIconData(), emojiSize: 15, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 27e1d3d341..97cc243c9e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/built_in_svgs.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; @@ -234,6 +235,7 @@ class _UserIcon extends StatelessWidget { queryParameters: { MobileEmojiPickerScreen.pageTitle: LocaleKeys.titleBar_userIcon.tr(), + MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name], }, ).toString(), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart index 99bd4de494..e589ce7cd2 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart @@ -178,16 +178,18 @@ class MobileViewPage extends StatelessWidget { overflow: TextOverflow.ellipsis, text: TextSpan( children: [ - WidgetSpan( - child: SizedBox( - width: 20, - child: EmojiIconWidget( - emoji: icon, - emojiSize: 17.0, + if (icon.isNotEmpty) ...[ + WidgetSpan( + child: SizedBox( + width: 20, + child: EmojiIconWidget( + emoji: icon, + emojiSize: 18.0, + ), ), ), - ), - if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)), + const WidgetSpan(child: HSpace(8.0)), + ], TextSpan( text: name, style: Theme.of(context).textTheme.bodyMedium!.copyWith( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index a2f3d2aeeb..bf7b1c9d63 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -79,13 +79,10 @@ class RawEmojiIconWidget extends StatelessWidget { try { switch (emoji.type) { case FlowyIconType.emoji: - return SizedBox( - width: emojiSize, - child: EmojiText( - emoji: emoji.emoji, - fontSize: emojiSize, - textAlign: TextAlign.center, - ), + return EmojiText( + emoji: emoji.emoji, + fontSize: emojiSize, + textAlign: TextAlign.center, ); case FlowyIconType.icon: final iconData = IconsData.fromJson(jsonDecode(emoji.emoji)); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index 6647796833..e3045a9c02 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -48,12 +48,15 @@ class _PageStyleIconState extends State { const HSpace(16.0), FlowyText(LocaleKeys.document_plugins_emoji.tr()), const Spacer(), - RawEmojiIconWidget( - emoji: icon.isNotEmpty - ? icon - : EmojiIconData.emoji(LocaleKeys.pageStyle_none.tr()), - emojiSize: icon.isNotEmpty ? 22.0 : 16.0, - ), + icon.isEmpty + ? FlowyText( + LocaleKeys.pageStyle_none.tr(), + fontSize: 16.0, + ) + : RawEmojiIconWidget( + emoji: icon, + emojiSize: 22.0, + ), const HSpace(6.0), const FlowySvg(FlowySvgs.m_page_style_arrow_right_s), const HSpace(12.0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart index 601868c887..ad69d4c784 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart @@ -1,8 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; import 'package:appflowy/plugins/trash/application/trash_listener.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -15,7 +17,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; @@ -244,12 +245,9 @@ class SubPageBlockComponentState extends State children: [ const HSpace(10), view.icon.value.isNotEmpty - ? FlowyText.emoji( - view.icon.value, - fontSize: textStyle.fontSize, - lineHeight: textStyle.height, - color: - AFThemeExtension.of(context).strongText, + ? RawEmojiIconWidget( + emoji: view.icon.toEmojiIconData(), + emojiSize: textStyle.fontSize ?? 16.0, ) : view.defaultIcon(), const HSpace(6), diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart index 7764d73838..a053595bbd 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart @@ -39,8 +39,9 @@ class IconGroup { final filteredIcons = icons .where( (icon) => - icon.keywords.any((k) => k.contains(lowercaseKey)) || - icon.name.contains(lowercaseKey), + icon.keywords + .any((k) => k.toLowerCase().contains(lowercaseKey)) || + icon.name.toLowerCase().contains(lowercaseKey), ) .toList(); return IconGroup(name: name, icons: filteredIcons); @@ -84,3 +85,23 @@ class Icon { return '${iconGroup!.name}/$name'; } } + +class RecentIcon { + factory RecentIcon.fromJson(Map json) => + RecentIcon(_$IconFromJson(json), json['groupName'] ?? ''); + + RecentIcon(this.icon, this.groupName); + + final Icon icon; + final String groupName; + + String get name => icon.name; + + List get keywords => icon.keywords; + + String get content => icon.content; + + Map toJson() => _$IconToJson( + Icon(name: name, keywords: keywords, content: content), + )..addAll({'groupName': groupName}); +} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart index 4e34badacd..f06e333477 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart @@ -118,10 +118,13 @@ class _FlowyIconPickerState extends State { iconGroups.add( IconGroup( name: _kRecentIconGroupName, - icons: recentIcons.sublist( - 0, - min(recentIcons.length, widget.iconPerLine), - ), + icons: recentIcons + .sublist( + 0, + min(recentIcons.length, widget.iconPerLine), + ) + .map((e) => e.icon) + .toList(), ), ); } @@ -171,7 +174,7 @@ class _FlowyIconPickerState extends State { color, ).toResult(isRandom: true), ); - RecentIcons.putIcon(value.$2); + RecentIcons.putIcon(RecentIcon(value.$2, value.$1.name)); }, onKeywordChanged: (keyword) => { debounce.call(() { @@ -303,30 +306,38 @@ class _IconPickerState extends State { icon: icon, mutex: mutex, onSelectedColor: (context, color) { + String groupName = iconGroup.name; + if (groupName == _kRecentIconGroupName) { + groupName = getGroupName(index); + } widget.onSelectedIcon( IconsData( - iconGroup.name, + groupName, icon.content, icon.name, color, ), ); - RecentIcons.putIcon(icon); + RecentIcons.putIcon(RecentIcon(icon, groupName)); PopoverContainer.of(context).close(); }, ) : _IconNoBackground( icon: icon, onSelectedIcon: () { + String groupName = iconGroup.name; + if (groupName == _kRecentIconGroupName) { + groupName = getGroupName(index); + } widget.onSelectedIcon( IconsData( - iconGroup.name, + groupName, icon.content, icon.name, null, ), ); - RecentIcons.putIcon(icon); + RecentIcons.putIcon(RecentIcon(icon, groupName)); }, ); }, @@ -341,6 +352,16 @@ class _IconPickerState extends State { }, ); } + + String getGroupName(int index) { + final recentIcons = RecentIcons.getIconsSync(); + try { + return recentIcons[index].groupName; + } catch (e) { + Log.error('getGroupName with index: $index error', e); + return ''; + } + } } class _IconNoBackground extends StatelessWidget { @@ -392,12 +413,20 @@ class _Icon extends StatefulWidget { class _IconState extends State<_Icon> { final PopoverController _popoverController = PopoverController(); + @override + void dispose() { + super.dispose(); + _popoverController.close(); + } + @override Widget build(BuildContext context) { return AppFlowyPopover( direction: PopoverDirection.bottomWithCenterAligned, + controller: _popoverController, offset: const Offset(0, 6), mutex: widget.mutex, + clickHandler: PopoverClickHandler.gestureDetector, child: _IconNoBackground( icon: widget.icon, onSelectedIcon: () => _popoverController.show(), diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart index aae3937fac..199bb7f725 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart +++ b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart @@ -22,13 +22,10 @@ class RecentIcons { await _put(FlowyIconType.emoji, id); } - static Future putIcon(Icon icon) async { + static Future putIcon(RecentIcon icon) async { await _put( FlowyIconType.icon, - jsonEncode( - Icon(name: icon.name, keywords: icon.keywords, content: icon.content) - .toJson(), - ), + jsonEncode(icon.toJson()), ); } @@ -37,12 +34,22 @@ class RecentIcons { return _dataMap[FlowyIconType.emoji.name] ?? []; } - static Future> getIcons() async { + static Future> getIcons() async { await _load(); + return getIconsSync(); + } + + static List getIconsSync() { final iconList = _dataMap[FlowyIconType.icon.name] ?? []; try { return iconList - .map((e) => Icon.fromJson(jsonDecode(e) as Map)) + .map( + (e) => RecentIcon.fromJson(jsonDecode(e) as Map), + ) + + /// skip the data that is already stored locally but has an empty + /// groupName to accommodate the issue of destructive data modifications + .skipWhile((e) => e.groupName.isEmpty) .toList(); } catch (e) { Log.error('RecentIcons getIcons with :$iconList', e); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index bcca2d0a69..6d4fc16841 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -29,6 +29,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter/foundation.dart'; @@ -288,10 +289,15 @@ GoRoute _mobileEmojiPickerPageRoute() { final selectedType = state .uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType] ?.toPickerTabType(); - final tabs = selectTabs - .split('-') - .map((e) => PickerTabType.values.byName(e)) - .toList(); + List tabs = []; + try { + tabs = selectTabs + .split('-') + .map((e) => PickerTabType.values.byName(e)) + .toList(); + } on ArgumentError catch (e) { + Log.error('convert selectTabs to pickerTab error', e); + } return MaterialExtendedPage( child: tabs.isEmpty ? MobileEmojiPickerScreen(title: title, selectedType: selectedType) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart index 03b96c9607..cf4a2aa5b1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart @@ -173,21 +173,22 @@ class _SidebarSpaceHeaderState extends State { await _showRenameDialog(); break; case SpaceMoreActionType.changeIcon: - final result = data as EmojiIconData; - if (data.type == FlowyIconType.icon) { - try { - final iconsData = IconsData.fromJson(jsonDecode(result.emoji)); - context.read().add( - SpaceEvent.changeIcon( - icon: '${iconsData.groupName}/${iconsData.iconName}', - iconColor: iconsData.color, - ), - ); - } on FormatException catch (e) { - context - .read() - .add(const SpaceEvent.changeIcon(icon: '')); - Log.warn('SidebarSpaceHeader changeIcon error:$e'); + if (data is SelectedEmojiIconResult) { + if (data.type == FlowyIconType.icon) { + try { + final iconsData = IconsData.fromJson(jsonDecode(data.emoji)); + context.read().add( + SpaceEvent.changeIcon( + icon: '${iconsData.groupName}/${iconsData.iconName}', + iconColor: iconsData.color, + ), + ); + } on FormatException catch (e) { + context + .read() + .add(const SpaceEvent.changeIcon(icon: '')); + Log.warn('SidebarSpaceHeader changeIcon error:$e'); + } } } break; diff --git a/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart b/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart index 9212467d84..51ec4dd6cd 100644 --- a/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart @@ -46,16 +46,17 @@ void main() { }); test('putIcons', () async { - List icons = await RecentIcons.getIcons(); + List icons = await RecentIcons.getIcons(); assert(icons.isEmpty); await loadIconGroups(); final groups = kIconGroups!; - final List localIcons = []; + final List localIcons = []; for (final e in groups) { - localIcons.addAll(e.icons); + localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList()); } - bool equalIcon(Icon a, Icon b) => + bool equalIcon(RecentIcon a, RecentIcon b) => + a.groupName == b.groupName && a.name == b.name && a.keywords.equals(b.keywords) && a.content == b.content; From cd06161deacc4e1559be4ee1dfc13eab0e3eed3f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 7 Jan 2025 09:39:39 +0800 Subject: [PATCH 18/21] chore: upgrade andoird targetSdkVersion to 35 --- frontend/appflowy_flutter/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index 3110b5b8ff..0b96e32472 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/frontend/appflowy_flutter/android/app/build.gradle @@ -53,7 +53,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.appflowy.appflowy" minSdkVersion 29 - targetSdkVersion 34 + targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true From b53e20f7461c080433607fd037709dcd959c0a1c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 7 Jan 2025 10:21:18 +0800 Subject: [PATCH 19/21] chore: update editor version --- frontend/appflowy_flutter/pubspec.lock | 4 ++-- frontend/appflowy_flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 5c8282724b..ee44eb0d19 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -61,8 +61,8 @@ packages: dependency: "direct main" description: path: "." - ref: "1208cc6" - resolved-ref: "1208cc60ebaf2ef942a6d391be2aeda9621bb66b" + ref: "4bcbfb0" + resolved-ref: "4bcbfb0679d07d9d4010869ea4bc2f2b7e32c479" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "4.0.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index a29396b15c..5a58f2798b 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -174,7 +174,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "1208cc6" + ref: "4bcbfb0" appflowy_editor_plugins: git: From 84d55b5022375d187e0ed4f5429da0d06a4a3ebf Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 7 Jan 2025 15:26:21 +0800 Subject: [PATCH 20/21] fix: disable deleting mutilple nodes in table --- .../shortcuts/backspace_command.dart | 321 ++++++++++++++++++ .../shortcuts/command_shortcuts.dart | 3 + 2 files changed, 324 insertions(+) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart 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 new file mode 100644 index 0000000000..58c3d2fb5d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/backspace_command.dart @@ -0,0 +1,321 @@ +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 8b168bcd30..86930a470b 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,6 +1,7 @@ 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'; @@ -37,6 +38,8 @@ List commandShortcutEvents = [ ...customTextAlignCommands, + customBackspaceCommand, + // remove standard shortcuts for copy, cut, paste, todo ...standardCommandShortcutEvents ..removeWhere( From 627b8ab43cda7e71bfa7ba81a02fc1fa81022198 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:56:17 +0800 Subject: [PATCH 21/21] fix(flutter_mobile): linked grid cannot be displayed in document (#7162) --- .../grid/presentation/mobile_grid_page.dart | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart index f70d98ab77..3874dc801e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart @@ -5,7 +5,6 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/shortcuts.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/startup/startup.dart'; @@ -42,6 +41,7 @@ class MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { view: view, databaseController: controller, initialRowId: initialRowId, + shrinkWrap: shrinkWrap, ); } @@ -68,12 +68,14 @@ class MobileGridPage extends StatefulWidget { required this.databaseController, this.onDeleted, this.initialRowId, + this.shrinkWrap = false, }); final ViewPB view; final DatabaseController databaseController; final VoidCallback? onDeleted; final String? initialRowId; + final bool shrinkWrap; @override State createState() => _MobileGridPageState(); @@ -104,7 +106,10 @@ class _MobileGridPageState extends State { finish: (result) { _openRow(context, widget.initialRowId, true); return result.successOrFail.fold( - (_) => GridShortcuts(child: GridPageContent(view: widget.view)), + (_) => GridPageContent( + view: widget.view, + shrinkWrap: widget.shrinkWrap, + ), (err) => Center( child: AppFlowyErrorPage( error: err, @@ -145,9 +150,11 @@ class GridPageContent extends StatefulWidget { const GridPageContent({ super.key, required this.view, + this.shrinkWrap = false, }); final ViewPB view; + final bool shrinkWrap; @override State createState() => _GridPageContentState(); @@ -196,6 +203,7 @@ class _GridPageContentState extends State { children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ _GridHeader( contentScrollController: contentScrollController, @@ -207,11 +215,12 @@ class _GridPageContentState extends State { ), ], ), - Positioned( - bottom: 16, - right: 16, - child: getGridFabs(context), - ), + if (!widget.shrinkWrap) + Positioned( + bottom: 16, + right: 16, + child: getGridFabs(context), + ), ], ), ); @@ -256,7 +265,7 @@ class _GridRows extends StatelessWidget { buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { final double contentWidth = getMobileGridContentWidth(state.fields); - return Expanded( + return Flexible( child: _WrapScrollView( scrollController: scrollController, contentWidth: contentWidth, @@ -305,6 +314,7 @@ class _GridRows extends StatelessWidget { return ReorderableListView.builder( scrollController: scrollController.verticalController, buildDefaultDragHandles: false, + shrinkWrap: true, proxyDecorator: (child, index, animation) => Material( color: Colors.transparent, child: child,