mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
5b3b0e54d9
commit
86a0569d84
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -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) {
|
||||
|
@ -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()),
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user