diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart index 594f13995f99e..f205b35354c8e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart @@ -1,6 +1,8 @@ import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -38,6 +40,10 @@ void main() { await tester.changeWorkspaceIcon(icon); await tester.changeWorkspaceName(name); + await tester.pumpUntilNotFound( + find.text(LocaleKeys.workspace_renameSuccess.tr()), + ); + workspaceIcon = tester.widget( find.byType(WorkspaceIcon), ); 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 accf9dfe0ec71..2de6fb8fa7c08 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 @@ -101,5 +101,52 @@ void main() { final memberCount = find.text('1 member'); expect(memberCount, findsNWidgets(2)); }); + + testWidgets('only display one menu item in the workspace menu', + (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + const name = 'AppFlowy.IO'; + // the workspace will be opened after created + await tester.createCollaborativeWorkspace(name); + + final loading = find.byType(Loading); + await tester.pumpUntilNotFound(loading); + + await tester.openCollaborativeWorkspaceMenu(); + + // hover on the workspace and click the more button + final workspaceItem = find.byWidgetPredicate( + (w) => w is WorkspaceMenuItem && w.workspace.name == name, + ); + 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); + + // click it again + await tester.tapButton(moreButton); + + // nothing should happen + expect( + find.text(LocaleKeys.button_rename.tr()), + findsOneWidget, + ); + }, + ); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart b/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart index bf86020141c47..a17fe909e76b2 100644 --- a/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart +++ b/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart @@ -1,7 +1,7 @@ import 'package:appflowy_backend/log.dart'; import 'package:integration_test/integration_test.dart'; -import 'mobile/document/page_style_test.dart' as page_style_test; +import 'mobile/document/document_test_runner.dart' as document_test_runner; import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test; import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; @@ -16,5 +16,5 @@ Future runIntegration1OnMobile() async { anonymous_sign_in_test.main(); create_new_page_test.main(); - page_style_test.main(); + document_test_runner.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart index 67506879d59d3..1b2f22b944e15 100644 --- a/frontend/appflowy_flutter/integration_test/shared/workspace.dart +++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart @@ -40,9 +40,13 @@ extension AppFlowyWorkspace on WidgetTester { moreButton, onHover: () async { await tapButton(moreButton); - await tapButton( - find.findTextInFlowyText(LocaleKeys.button_rename.tr()), + // wait for the menu to open + final renameButton = find.findTextInFlowyText( + LocaleKeys.button_rename.tr(), ); + await pumpUntilFound(renameButton); + expect(renameButton, findsOneWidget); + await tapButton(renameButton); final input = find.byType(TextFormField); expect(input, findsOneWidget); await enterText(input, name); diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart index a05a4ad287b7d..48626b4895c1a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; @@ -5,6 +6,7 @@ import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -126,6 +128,15 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { commandShortcutEvents: [customCopyCommand], disableAutoScroll: true, editorState: editorState, + contextMenuItems: [ + [ + ContextMenuItem( + getName: LocaleKeys.document_plugins_contextMenu_copy.tr, + onPressed: (editorState) => + customCopyCommand.execute(editorState), + ), + ] + ], ), ); } 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 a0be2b43b3a59..ee5bc3fab3bff 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 @@ -22,9 +22,11 @@ class WorkspaceMoreActionList extends StatelessWidget { const WorkspaceMoreActionList({ super.key, required this.workspace, + required this.isShowingMoreActions, }); final UserWorkspacePB workspace; + final ValueNotifier isShowingMoreActions; @override Widget build(BuildContext context) { @@ -46,6 +48,13 @@ class WorkspaceMoreActionList extends StatelessWidget { .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) .toList(), constraints: const BoxConstraints(minWidth: 220), + animationDuration: Durations.short3, + slideDistance: 2, + beginScaleFactor: 1.0, + beginOpacity: 0.8, + onClosed: () { + isShowingMoreActions.value = false; + }, buildChild: (controller) { return SizedBox.square( dimension: 24.0, @@ -55,7 +64,11 @@ class WorkspaceMoreActionList extends StatelessWidget { FlowySvgs.workspace_three_dots_s, ), onTap: () { - controller.show(); + if (!isShowingMoreActions.value) { + controller.show(); + } + + isShowingMoreActions.value = true; }, ), ); 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 af8e0cb8c41f7..4119239694d78 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 @@ -25,7 +25,7 @@ const createWorkspaceButtonKey = ValueKey('createWorkspaceButton'); @visibleForTesting const importNotionButtonKey = ValueKey('importNotinoButton'); -class WorkspacesMenu extends StatelessWidget { +class WorkspacesMenu extends StatefulWidget { const WorkspacesMenu({ super.key, required this.userProfile, @@ -37,6 +37,19 @@ class WorkspacesMenu extends StatelessWidget { final UserWorkspacePB currentWorkspace; final List workspaces; + @override + State createState() => _WorkspacesMenuState(); +} + +class _WorkspacesMenuState extends State { + final ValueNotifier isShowingMoreActions = ValueNotifier(false); + + @override + void dispose() { + isShowingMoreActions.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( @@ -72,13 +85,14 @@ class WorkspacesMenu extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - for (final workspace in workspaces) ...[ + for (final workspace in widget.workspaces) ...[ WorkspaceMenuItem( key: ValueKey(workspace.workspaceId), workspace: workspace, - userProfile: userProfile, - isSelected: - workspace.workspaceId == currentWorkspace.workspaceId, + userProfile: widget.userProfile, + isSelected: workspace.workspaceId == + widget.currentWorkspace.workspaceId, + isShowingMoreActions: isShowingMoreActions, ), const VSpace(6.0), ], @@ -99,12 +113,12 @@ class WorkspacesMenu extends StatelessWidget { } String _getUserInfo() { - if (userProfile.email.isNotEmpty) { - return userProfile.email; + if (widget.userProfile.email.isNotEmpty) { + return widget.userProfile.email; } - if (userProfile.name.isNotEmpty) { - return userProfile.name; + if (widget.userProfile.name.isNotEmpty) { + return widget.userProfile.name; } return LocaleKeys.defaultUsername.tr(); @@ -117,11 +131,13 @@ class WorkspaceMenuItem extends StatefulWidget { required this.workspace, required this.userProfile, required this.isSelected, + required this.isShowingMoreActions, }); final UserProfilePB userProfile; final UserWorkspacePB workspace; final bool isSelected; + final ValueNotifier isShowingMoreActions; @override State createState() => _WorkspaceMenuItemState(); @@ -211,7 +227,10 @@ class _WorkspaceMenuItemState extends State { ), ); }, - child: WorkspaceMoreActionList(workspace: widget.workspace), + child: WorkspaceMoreActionList( + workspace: widget.workspace, + isShowingMoreActions: widget.isShowingMoreActions, + ), ), const HSpace(8.0), if (widget.isSelected) ...[ diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs index 0f328cdf2aa58..cee8f1d381006 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs @@ -5,13 +5,14 @@ use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::{error, trace}; #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[allow(dead_code)] pub struct WatchContext { - #[allow(dead_code)] watcher: notify::RecommendedWatcher, pub path: PathBuf, } #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[allow(dead_code)] pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver)> { use notify::{Event, Watcher};