feat: f2 to rename current view (#4522)

* feat: cmd+shift+r to rename current view

* test: change cmd to f2 and add test

* chore: code review

* fix: unawaited future
This commit is contained in:
Mathias Mogensen 2024-01-29 20:55:37 +01:00 committed by GitHub
parent 5b3b0e54d9
commit 86a0569d84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 248 additions and 63 deletions

View File

@ -0,0 +1,51 @@
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/base.dart';
import '../util/common_operations.dart';
import '../util/keyboard.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Rename current view item', () {
testWidgets('by F2 shortcut', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await FlowyTestKeyboard.simulateKeyDownEvent(
[LogicalKeyboardKey.f2],
tester: tester,
);
await tester.pumpAndSettle();
expect(find.byType(RenameViewPopover), findsOneWidget);
await tester.enterText(
find.descendant(
of: find.byType(RenameViewPopover),
matching: find.byType(FlowyTextField),
),
'hello',
);
await tester.pumpAndSettle();
// Dismiss rename popover
await tester.tap(find.byType(AppFlowyEditor));
await tester.pumpAndSettle();
expect(
find.descendant(
of: find.byType(SingleInnerViewItem),
matching: find.text('hello'),
),
findsOneWidget,
);
});
});
}

View File

@ -23,6 +23,7 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan
import 'package:appflowy/workspace/application/settings/appearance/desktop_appearance.dart';
import 'package:appflowy/workspace/application/settings/appearance/mobile_appearance.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
@ -32,6 +33,7 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -187,6 +189,8 @@ void _resolveHomeDeps(GetIt getIt) {
getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());
getIt.registerSingleton<ReminderBloc>(ReminderBloc());
getIt.registerSingleton<RenameViewBloc>(RenameViewBloc(PopoverController()));
}
void _resolveFolderDeps(GetIt getIt) {

View File

@ -1,5 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart';
@ -10,6 +13,7 @@ import 'package:appflowy/workspace/application/notifications/notification_action
import 'package:appflowy/workspace/application/notifications/notification_service.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@ -17,8 +21,6 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@ -145,6 +147,7 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
BlocProvider<DocumentAppearanceCubit>(
create: (_) => DocumentAppearanceCubit()..fetch(),
),
BlocProvider.value(value: getIt<RenameViewBloc>()),
BlocProvider.value(value: getIt<NotificationActionBloc>()),
BlocProvider.value(
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),

View File

@ -0,0 +1,37 @@
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'rename_view_bloc.freezed.dart';
class RenameViewBloc extends Bloc<RenameViewEvent, RenameViewState> {
RenameViewBloc(PopoverController controller)
: _controller = controller,
super(RenameViewState(controller: controller)) {
on<RenameViewEvent>((event, emit) {
event.when(
open: () => _controller.show(),
);
});
}
final PopoverController _controller;
@override
Future<void> close() async {
_controller.close();
await super.close();
}
}
@freezed
class RenameViewEvent with _$RenameViewEvent {
const factory RenameViewEvent.open() = _Open;
}
@freezed
class RenameViewState with _$RenameViewState {
const factory RenameViewState({
required PopoverController controller,
}) = _RenameViewState;
}

View File

@ -1,8 +1,12 @@
import 'dart:io';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:provider/provider.dart';
@ -93,6 +97,16 @@ class HomeHotKeys extends StatelessWidget {
keyDownHandler: (_) => _selectTab(context, 1),
).register();
// Rename current view
HotKeyItem(
hotKey: HotKey(
KeyCode.f2,
scope: HotKeyScope.inapp,
),
keyDownHandler: (_) =>
getIt<RenameViewBloc>().add(const RenameViewEvent.open()),
).register();
return child;
}

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
@ -5,6 +7,7 @@ import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -15,13 +18,13 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.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:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef ViewItemOnSelected = void Function(ViewPB);
@ -294,6 +297,9 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
return _buildViewItem(false);
}
final isSelected =
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id;
return FlowyHover(
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.secondary,
@ -301,14 +307,12 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
resetHoverOnRebuild: widget.showActions || !isIconPickerOpened,
buildWhenOnHover: () =>
!widget.showActions && !_isDragging && !isIconPickerOpened,
builder: (_, onHover) => _buildViewItem(onHover),
isSelected: () =>
widget.showActions ||
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id,
builder: (_, onHover) => _buildViewItem(onHover, isSelected),
isSelected: () => widget.showActions || isSelected,
);
}
Widget _buildViewItem(bool onHover) {
Widget _buildViewItem(bool onHover, [bool isSelected = false]) {
final children = [
// expand icon
_buildLeftIcon(),
@ -335,7 +339,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
}
}
return GestureDetector(
final child = GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => widget.onSelected(widget.view),
onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view),
@ -349,6 +353,26 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
),
),
);
if (isSelected) {
final popoverController = getIt<RenameViewBloc>().state.controller;
return AppFlowyPopover(
controller: popoverController,
triggerActions: PopoverTriggerFlags.none,
offset: const Offset(0, 5),
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (_) => RenameViewPopover(
viewId: widget.view.id,
name: widget.view.name,
emoji: widget.view.icon.value,
popoverController: popoverController,
showIconChanger: false,
),
child: child,
);
}
return child;
}
Widget _buildViewIconButton() {

View File

@ -0,0 +1,93 @@
import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
class RenameViewPopover extends StatefulWidget {
const RenameViewPopover({
super.key,
required this.viewId,
required this.name,
required this.popoverController,
required this.emoji,
this.icon,
this.showIconChanger = true,
});
final String viewId;
final String name;
final PopoverController popoverController;
final String emoji;
final Widget? icon;
final bool showIconChanger;
@override
State<RenameViewPopover> createState() => _RenameViewPopoverState();
}
class _RenameViewPopoverState extends State<RenameViewPopover> {
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
_controller.text = widget.name;
_controller.selection =
TextSelection(baseOffset: 0, extentOffset: widget.name.length);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showIconChanger) ...[
EmojiPickerButton(
emoji: widget.emoji,
defaultIcon: widget.icon,
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 18),
onSubmitted: _updateViewIcon,
),
const HSpace(6),
],
SizedBox(
height: 36.0,
width: 220,
child: FlowyTextField(
controller: _controller,
onSubmitted: _updateViewName,
onCanceled: () => _updateViewName(_controller.text),
),
),
],
);
}
Future<void> _updateViewName(String name) async {
if (name.isNotEmpty && name != widget.name) {
await ViewBackendService.updateView(
viewId: widget.viewId,
name: _controller.text,
);
widget.popoverController.close();
}
}
Future<void> _updateViewIcon(String emoji, PopoverController? _) async {
await ViewBackendService.updateViewIcon(
viewId: widget.viewId,
viewIcon: emoji,
);
widget.popoverController.close();
}
}

View File

@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// workspaces / ... / view_title
@ -231,58 +231,17 @@ class _ViewTitleState extends State<_ViewTitle> {
maxHeight: 44,
),
controller: popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 18),
popupBuilder: (context) {
// icon + textfield
_resetTextEditingController();
return Row(
mainAxisSize: MainAxisSize.min,
children: [
EmojiPickerButton(
emoji: icon,
defaultIcon: widget.view.defaultIcon(),
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 18),
onSubmitted: (emoji, _) async {
await ViewBackendService.updateViewIcon(
viewId: widget.view.id,
viewIcon: emoji,
);
popoverController.close();
},
),
const HSpace(4.0),
SizedBox(
height: 36.0,
width: 220,
child: FlowyTextField(
controller: textEditingController,
onSubmitted: (text) async {
if (text.isNotEmpty && text != name) {
await ViewBackendService.updateView(
viewId: widget.view.id,
name: text,
);
}
popoverController.close();
},
onChanged: (text) async {
inputtingName = text;
},
onCanceled: () async {
if (inputtingName.isNotEmpty && inputtingName != name) {
await ViewBackendService.updateView(
viewId: widget.view.id,
name: inputtingName,
);
popoverController.close();
}
},
),
),
const HSpace(4.0),
],
return RenameViewPopover(
viewId: widget.view.id,
name: widget.view.name,
popoverController: popoverController,
icon: widget.view.defaultIcon(),
emoji: icon,
);
},
child: FlowyButton(