From da7def01fc56d5699f2c267112e5c364828cad06 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 29 Jul 2023 15:20:24 +0800 Subject: [PATCH 01/10] feat: implement draggable folder --- .../lib/core/config/kv_keys.dart | 6 + .../workspace/application/menu/menu_bloc.dart | 3 +- .../workspace/application/view/view_bloc.dart | 127 +++++++- .../workspace/application/view/view_ext.dart | 18 +- .../application/view/view_service.dart | 16 + .../presentation/home/home_screen.dart | 8 +- .../home/menu/app/section/section.dart | 5 +- .../home/menu/sidebar/sidebar.dart | 87 +++++ .../home/menu/sidebar/sidebar_folder.dart | 37 +++ .../menu/sidebar/sidebar_new_page_button.dart | 56 ++++ .../home/menu/sidebar/sidebar_top_menu.dart | 82 +++++ .../home/menu/sidebar/sidebar_user.dart | 138 ++++++++ .../home/menu/view/draggable_view_item.dart | 175 ++++++++++ .../home/menu/view/view_action_type.dart | 54 ++++ .../home/menu/view/view_add_button.dart | 153 +++++++++ .../home/menu/view/view_item.dart | 304 ++++++++++++++++++ .../menu/view/view_more_action_button.dart | 67 ++++ .../draggable_item/draggable_item.dart | 96 ++++++ .../lib/style_widget/extension.dart | 25 ++ frontend/resources/translations/en.json | 6 +- 20 files changed, 1452 insertions(+), 11 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index 6e0e389654741..032400140cf5e 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -29,4 +29,10 @@ class KVKeys { 'kDocumentAppearanceFontSize'; static const String kDocumentAppearanceFontFamily = 'kDocumentAppearanceFontFamily'; + + /// The key for saving the expanded views + /// + /// The value is a json string with the following format: + /// {'viewId': true, 'viewId2': false} + static const String expandedViews = 'expandedViews'; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart index ac5e02c9ce68b..c0f7cec91b925 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -43,7 +44,7 @@ class MenuBloc extends Bloc { desc: event.desc ?? "", ); result.fold( - (app) => {}, + (app) => emit(state.copyWith(plugin: app.plugin())), (error) { Log.error(error); emit(state.copyWith(successOrFailure: right(error))); diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index f4f4e2ea2000a..9a1bd29ef81f4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -1,3 +1,8 @@ +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:dartz/dartz.dart'; @@ -20,21 +25,34 @@ class ViewBloc extends Bloc { super(ViewState.init(view)) { on((event, emit) async { await event.map( - initial: (e) { + initial: (e) async { listener.start( onViewUpdated: (result) { add(ViewEvent.viewDidUpdate(left(result))); }, ); - emit(state); + final isExpanded = await _getViewIsExpanded(view); + await _loadViewsWhenExpanded(emit, isExpanded); }, setIsEditing: (e) { emit(state.copyWith(isEditing: e.isEditing)); }, + setIsExpanded: (e) async { + if (e.isExpanded) { + await _loadViewsWhenExpanded(emit, true); + } else { + emit(state.copyWith(isExpanded: e.isExpanded)); + } + await _setViewIsExpanded(view, e.isExpanded); + }, viewDidUpdate: (e) { e.result.fold( (view) => emit( - state.copyWith(view: view, successOrFailure: left(unit)), + state.copyWith( + view: view, + childViews: view.childViews, + successOrFailure: left(unit), + ), ), (error) => emit( state.copyWith(successOrFailure: right(error)), @@ -71,6 +89,36 @@ class ViewBloc extends Bloc { ), ); }, + move: (value) async { + final result = await ViewBackendService.moveViewV2( + viewId: value.from.id, + newParentId: value.newParentId, + prevViewId: value.prevId, + ); + emit( + result.fold( + (l) => state.copyWith(successOrFailure: left(unit)), + (error) => state.copyWith(successOrFailure: right(error)), + ), + ); + }, + createView: (e) async { + final result = await ViewBackendService.createView( + parentViewId: view.id, + name: e.name, + desc: '', + layoutType: e.layoutType, + initialDataBytes: null, + ext: {}, + openAfterCreate: e.openAfterCreated, + ); + emit( + result.fold( + (l) => state.copyWith(successOrFailure: left(unit)), + (error) => state.copyWith(successOrFailure: right(error)), + ), + ); + }, ); }); } @@ -80,15 +128,84 @@ class ViewBloc extends Bloc { await listener.stop(); return super.close(); } + + Future _loadViewsWhenExpanded( + Emitter emit, + bool isExpanded, + ) async { + if (!isExpanded) { + return; + } + if (state.childViews.isNotEmpty) { + // notify the old child views + emit( + state.copyWith( + childViews: state.childViews, + isExpanded: true, + ), + ); + } + final viewsOrFailed = + await ViewBackendService.getChildViews(viewId: state.view.id); + viewsOrFailed.fold( + (childViews) => emit( + state.copyWith( + childViews: childViews, + isExpanded: true, + ), + ), + (error) => emit( + state.copyWith( + successOrFailure: right(error), + isExpanded: true, + ), + ), + ); + } + + Future _setViewIsExpanded(ViewPB view, bool isExpanded) async { + final result = await getIt().get(KVKeys.expandedViews); + final map = result.fold( + (l) => {}, + (r) => jsonDecode(r), + ); + if (isExpanded) { + map[view.id] = true; + } else { + map.remove(view.id); + } + await getIt().set(KVKeys.expandedViews, jsonEncode(map)); + } + + Future _getViewIsExpanded(ViewPB view) { + return getIt().get(KVKeys.expandedViews).then((result) { + return result.fold((l) => false, (r) { + final map = jsonDecode(r); + return map[view.id] ?? false; + }); + }); + } } @freezed class ViewEvent with _$ViewEvent { const factory ViewEvent.initial() = Initial; const factory ViewEvent.setIsEditing(bool isEditing) = SetEditing; + const factory ViewEvent.setIsExpanded(bool isExpanded) = SetIsExpanded; const factory ViewEvent.rename(String newName) = Rename; const factory ViewEvent.delete() = Delete; const factory ViewEvent.duplicate() = Duplicate; + const factory ViewEvent.move( + ViewPB from, + String newParentId, + String? prevId, + ) = Move; + const factory ViewEvent.createView( + String name, + ViewLayoutPB layoutType, { + /// open the view after created + @Default(true) bool openAfterCreated, + }) = CreateView; const factory ViewEvent.viewDidUpdate(Either result) = ViewDidUpdate; } @@ -97,12 +214,16 @@ class ViewEvent with _$ViewEvent { class ViewState with _$ViewState { const factory ViewState({ required ViewPB view, + required List childViews, required bool isEditing, + required bool isExpanded, required Either successOrFailure, }) = _ViewState; factory ViewState.init(ViewPB view) => ViewState( view: view, + childViews: view.childViews, + isExpanded: false, isEditing: false, successOrFailure: left(unit), ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 4614ca033ed48..c8ed525e322f8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -40,11 +40,27 @@ extension FlowyPluginExtension on FlowyPlugin { extension ViewExtension on ViewPB { Widget renderThumbnail({Color? iconColor}) { const String thumbnail = "file_icon"; - const Widget widget = FlowySvg(name: thumbnail); return widget; } + Widget icon() { + String iconName = 'file_icon'; + switch (layout) { + case ViewLayoutPB.Board: + iconName = 'editor/board'; + case ViewLayoutPB.Calendar: + iconName = 'editor/calendar'; + case ViewLayoutPB.Grid: + iconName = 'editor/grid'; + case ViewLayoutPB.Document: + iconName = 'editor/documents'; + } + return FlowySvg( + name: iconName, + ); + } + PluginType get pluginType { switch (layout) { case ViewLayoutPB.Board: diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index a577493fd99dd..0f59052da07a3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -141,6 +141,7 @@ class ViewBackendService { return FolderEventUpdateView(payload).send(); } + // deprecated static Future> moveView({ required String viewId, required int fromIndex, @@ -154,6 +155,21 @@ class ViewBackendService { return FolderEventMoveView(payload).send(); } + /// support nested view + static Future> moveViewV2({ + required String viewId, + required String newParentId, + required String? prevViewId, + }) { + final payload = MoveNestedViewPayloadPB( + viewId: viewId, + newParentId: newParentId, + prevViewId: prevViewId, + ); + + return FolderEventMoveNestedView(payload).send(); + } + Future)>> fetchViewsWithLayoutType( ViewLayoutPB? layoutType, ) async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart index 29266927d722c..c5fbb37edde6a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart @@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart'; import 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart'; import 'package:appflowy_backend/log.dart'; @@ -23,7 +24,6 @@ import 'package:styled_widget/styled_widget.dart'; import '../widgets/edit_panel/edit_panel.dart'; import 'home_layout.dart'; import 'home_stack.dart'; -import 'menu/menu.dart'; class HomeScreen extends StatefulWidget { final UserProfilePB user; @@ -145,7 +145,11 @@ class _HomeScreenState extends State { required BuildContext context, }) { final workspaceSetting = widget.workspaceSetting; - final homeMenu = HomeMenu( + // final homeMenu = HomeMenu( + // user: widget.user, + // workspaceSetting: workspaceSetting, + // ); + final homeMenu = HomeSideBar( user: widget.user, workspaceSetting: workspaceSetting, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart index 41cf8773025ae..7c6216486b01c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart @@ -3,13 +3,12 @@ import 'package:appflowy/workspace/application/app/app_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reorderables/reorderables.dart'; -import 'item.dart'; - class ViewSection extends StatelessWidget { final ViewDataContext appViewData; const ViewSection({Key? key, required this.appViewData}) : super(key: key); @@ -44,7 +43,7 @@ class ViewSection extends StatelessWidget { ); } - ReorderableColumn _reorderableColumn( + Widget _reorderableColumn( BuildContext context, ViewSectionState state, ) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart new file mode 100644 index 0000000000000..7f089e567a2e8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -0,0 +1,87 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Home Sidebar is the left side bar of the home page. +/// +/// in the sidebar, we have: +/// - user icon, user name +/// - settings +/// - scrollable document list +/// - trash +class HomeSideBar extends StatelessWidget { + const HomeSideBar({ + super.key, + required this.user, + required this.workspaceSetting, + }); + + final UserProfilePB user; + + final WorkspaceSettingPB workspaceSetting; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => MenuBloc( + user: user, + workspace: workspaceSetting.workspace, + )..add(const MenuEvent.initial()), + child: BlocConsumer( + builder: (context, state) => _buildSidebar(context, state), + listenWhen: (p, c) => p.plugin.id != c.plugin.id, + listener: (context, state) => getIt().add( + TabsEvent.openPlugin(plugin: state.plugin), + ), + ), + ); + } + + Widget _buildSidebar(BuildContext context, MenuState state) { + final views = state.views; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + border: Border( + right: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // top menu + const SidebarTopMenu(), + // user, setting + SidebarUser(user: user), + // Favorite, Not supported yet + const VSpace(30), + // scrollable document list + Expanded( + child: SingleChildScrollView( + child: SidebarFolder( + views: views, + ), + ), + ), + const VSpace(10), + // new page button + const SidebarNewAppButton(), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart new file mode 100644 index 0000000000000..564fbe0b40ec9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart @@ -0,0 +1,37 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SidebarFolder extends StatelessWidget { + const SidebarFolder({ + super.key, + required this.views, + }); + + final List views; + + @override + Widget build(BuildContext context) { + return Column( + children: views + .map( + (view) => ViewItem( + key: ValueKey(view.id), + isFirstChild: view.id == views.first.id, + view: view, + level: 0, + onSelected: (view) { + getIt().latestOpenView = view; + context.read().add(MenuEvent.openPage(view.plugin())); + }, + ), + ) + .toList(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart new file mode 100644 index 0000000000000..5e4d6827dd69a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart @@ -0,0 +1,56 @@ +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/extension.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SidebarNewAppButton extends StatelessWidget { + const SidebarNewAppButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final child = FlowyTextButton( + LocaleKeys.newPageText.tr(), + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + fontColor: Theme.of(context).colorScheme.tertiary, + onPressed: () async => await _showCreateAppDialog(context), + heading: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surface, + ), + child: svgWidget('home/new_app'), + ), + padding: const EdgeInsets.all(0), + ); + + return SizedBox( + height: 60, + child: TopBorder( + color: Theme.of(context).dividerColor, + child: child, + ), + ); + } + + Future _showCreateAppDialog(BuildContext context) async { + return NavigatorTextFieldDialog( + title: LocaleKeys.newPageText.tr(), + value: '', + confirm: (value) { + if (value.isNotEmpty) { + context.read().add(MenuEvent.createApp(value, desc: '')); + } + }, + ).show(context); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart new file mode 100644 index 0000000000000..30583d76129fd --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -0,0 +1,82 @@ +import 'dart:io' show Platform; + +import 'package:appflowy/core/frameless_window.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Sidebar top menu is the top bar of the sidebar. +/// +/// in the top menu, we have: +/// - appflowy icon (Windows or Linux) +/// - close / expand sidebar button +class SidebarTopMenu extends StatelessWidget { + const SidebarTopMenu({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SizedBox( + height: HomeSizes.topBarHeight, + child: MoveWindowDetector( + child: Row( + children: [ + _buildLogoIcon(context), + const Spacer(), + _buildCollapseMenuButton(context), + ], + ), + ), + ); + }, + ); + } + + Widget _buildLogoIcon(BuildContext context) { + if (Platform.isMacOS) { + return const SizedBox.shrink(); + } + + final name = Theme.of(context).brightness == Brightness.dark + ? 'flowy_logo_dark_mode' + : 'flowy_logo_with_text'; + return FlowySvg(name: name); + } + + Widget _buildCollapseMenuButton(BuildContext context) { + final textSpan = TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', + ), + TextSpan( + // TODO(Lucas.Xu): it doesn't work on macOS. + text: Platform.isMacOS ? '⌘+\\' : 'Ctrl+\\', + ), + ], + ); + return Tooltip( + richMessage: textSpan, + child: FlowyIconButton( + width: 28, + hoverColor: Colors.transparent, + onPressed: () => context + .read() + .add(const HomeSettingEvent.collapseMenu()), + iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), + icon: const FlowySvg( + name: 'home/hide_menu', + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart new file mode 100644 index 0000000000000..6724392412fbb --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart @@ -0,0 +1,138 @@ +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class SidebarUser extends StatelessWidget { + const SidebarUser({ + super.key, + required this.user, + }); + + final UserProfilePB user; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(param1: user) + ..add( + const MenuUserEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildAvatar(context), + const HSpace(10), + Expanded( + child: _buildUserName(context), + ), + _buildSettingsButton(context), + ], + ), + ), + ); + } + + Widget _buildAvatar(BuildContext context) { + String iconUrl = context.read().state.userProfile.iconUrl; + if (iconUrl.isEmpty) { + iconUrl = defaultUserAvatar; + final String name = _userName( + context.read().state.userProfile, + ); + final Color color = ColorGenerator().generateColorFromString(name); + const initialsCount = 2; + // Taking the first letters of the name components and limiting to 2 elements + final nameInitials = name + .split(' ') + .where((element) => element.isNotEmpty) + .take(initialsCount) + .map((element) => element[0].toUpperCase()) + .join(''); + return Container( + width: 28, + height: 28, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: FlowyText.semibold( + nameInitials, + color: Colors.white, + fontSize: nameInitials.length == initialsCount ? 12 : 14, + ), + ); + } + return SizedBox.square( + dimension: 25, + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: svgWidget('emoji/$iconUrl'), + ), + ), + ); + } + + Widget _buildUserName(BuildContext context) { + final String name = _userName( + context.read().state.userProfile, + ); + return FlowyText.medium( + name, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.tertiary, + ); + } + + Widget _buildSettingsButton(BuildContext context) { + final userProfile = context.read().state.userProfile; + return Tooltip( + message: LocaleKeys.settings_menu_open.tr(), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return BlocProvider.value( + value: BlocProvider.of(context), + child: SettingsDialog(userProfile), + ); + }, + ); + }, + icon: SizedBox.square( + dimension: 20, + child: svgWidget( + 'home/settings', + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ), + ); + } + + /// Return the user name, if the user name is empty, return the default user name. + String _userName(UserProfilePB userProfile) { + String name = userProfile.name; + if (name.isEmpty) { + name = LocaleKeys.defaultUsername.tr(); + } + return name; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart new file mode 100644 index 0000000000000..35b1ba4f99205 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -0,0 +1,175 @@ +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +enum DraggableHoverPosition { + none, + top, + center, + bottom, +} + +class DraggableViewItem extends StatefulWidget { + const DraggableViewItem({ + super.key, + required this.view, + this.feedback, + required this.child, + this.isFirstChild = false, + }); + + final Widget child; + final WidgetBuilder? feedback; + final ViewPB view; + final bool isFirstChild; + + @override + State createState() => _DraggableViewItemState(); +} + +class _DraggableViewItemState extends State { + DraggableHoverPosition position = DraggableHoverPosition.none; + + @override + Widget build(BuildContext context) { + // add top border if the draggable item is on the top of the list + // highlight the draggable item if the draggable item is on the center + // add bottom border if the draggable item is on the bottom of the list + final child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + // only show the top border when the draggable item is the first child + if (widget.isFirstChild) + Divider( + height: 2, + thickness: 2, + color: position == DraggableHoverPosition.top + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + ), + Container( + color: position == DraggableHoverPosition.center + ? Theme.of(context).colorScheme.secondary.withOpacity(0.5) + : Colors.transparent, + child: widget.child, + ), + Divider( + height: 2, + thickness: 2, + color: position == DraggableHoverPosition.bottom + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + ), + ], + ); + + return DraggableItem( + data: widget.view, + onWillAccept: (data) => true, + onMove: (data) { + if (!_shouldAccept(data.data)) { + return; + } + final renderBox = context.findRenderObject() as RenderBox; + final offset = renderBox.globalToLocal(data.offset); + setState(() { + position = _computeHoverPosition(offset, renderBox.size); + Log.debug( + 'offset: $offset, position: $position, size: ${renderBox.size}', + ); + }); + }, + onLeave: (_) => setState( + () => position = DraggableHoverPosition.none, + ), + onAccept: (data) { + _move(data, widget.view); + setState( + () => position = DraggableHoverPosition.none, + ); + }, + feedback: IntrinsicWidth( + child: Opacity( + opacity: 0.5, + child: widget.feedback?.call(context) ?? child, + ), + ), + child: child, + ); + } + + void _move(ViewPB from, ViewPB to) { + switch (position) { + case DraggableHoverPosition.top: + context.read().add( + ViewEvent.move( + from, + to.parentViewId, + null, + ), + ); + break; + case DraggableHoverPosition.bottom: + context.read().add( + ViewEvent.move( + from, + to.parentViewId, + to.id, + ), + ); + break; + case DraggableHoverPosition.center: + context.read().add( + ViewEvent.move( + from, + to.id, + to.childViews.lastOrNull?.id, + ), + ); + break; + case DraggableHoverPosition.none: + break; + } + } + + DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) { + final threshold = size.height / 4.0; + if (widget.isFirstChild && offset.dy < -5.0) { + return DraggableHoverPosition.top; + } + if (offset.dy > threshold) { + return DraggableHoverPosition.bottom; + } + return DraggableHoverPosition.center; + } + + bool _shouldAccept(ViewPB data) { + // ignore moving the view to itself + if (data.id == widget.view.id) { + return false; + } + + // ignore moving the view to its child view + if (data.containsView(widget.view)) { + return false; + } + + return true; + } +} + +extension on ViewPB { + bool containsView(ViewPB view) { + if (id == view.id) { + return true; + } + + print('this = $this'); + print('target = $view'); + + return childViews.any((v) => v.containsView(view)); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart new file mode 100644 index 0000000000000..32371d8af877d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart @@ -0,0 +1,54 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flutter/material.dart'; + +enum ViewMoreActionType { + delete, + addToFavorites, // not supported yet. + duplicate, + copyLink, // not supported yet. + rename, + moveTo, // not supported yet. + openInNewTab, +} + +extension ViewMoreActionTypeExtension on ViewMoreActionType { + String get name { + switch (this) { + case ViewMoreActionType.delete: + return LocaleKeys.disclosureAction_delete.tr(); + case ViewMoreActionType.addToFavorites: + return LocaleKeys.disclosureAction_addToFavorites.tr(); + case ViewMoreActionType.duplicate: + return LocaleKeys.disclosureAction_duplicate.tr(); + case ViewMoreActionType.copyLink: + return LocaleKeys.disclosureAction_copyLink.tr(); + case ViewMoreActionType.rename: + return LocaleKeys.disclosureAction_rename.tr(); + case ViewMoreActionType.moveTo: + return LocaleKeys.disclosureAction_moveTo.tr(); + case ViewMoreActionType.openInNewTab: + return LocaleKeys.disclosureAction_openNewTab.tr(); + } + } + + Widget icon(Color iconColor) { + switch (this) { + case ViewMoreActionType.delete: + return const FlowySvg(name: 'editor/delete'); + case ViewMoreActionType.addToFavorites: + return const Icon(Icons.favorite); + case ViewMoreActionType.duplicate: + return const FlowySvg(name: 'editor/copy'); + case ViewMoreActionType.copyLink: + return const Icon(Icons.copy); + case ViewMoreActionType.rename: + return const FlowySvg(name: 'editor/edit'); + case ViewMoreActionType.moveTo: + return const Icon(Icons.move_to_inbox); + case ViewMoreActionType.openInNewTab: + return const FlowySvg(name: 'grid/expander'); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart new file mode 100644 index 0000000000000..72ad20b8e87ec --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart @@ -0,0 +1,153 @@ +import 'package:appflowy/plugins/document/document.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart'; +import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class ViewAddButton extends StatelessWidget { + const ViewAddButton({ + super.key, + required this.parentViewId, + required this.onEditing, + required this.onSelected, + }); + + final String parentViewId; + final void Function(bool value) onEditing; + final Function( + PluginBuilder, + String? name, + List? initialDataBytes, + bool openAfterCreated, + ) onSelected; + + List get _actions { + return [ + // document, grid, kanban, calendar + ...pluginBuilders().map( + (pluginBuilder) => ViewAddButtonActionWrapper( + pluginBuilder: pluginBuilder, + ), + ), + // import from ... + ...getIt().builders.whereType().map( + (pluginBuilder) => ViewImportActionWrapper( + pluginBuilder: pluginBuilder, + ), + ), + ]; + } + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithLeftAligned, + actions: _actions, + offset: const Offset(0, 8), + buildChild: (popover) { + return FlowyIconButton( + hoverColor: Colors.transparent, + iconPadding: const EdgeInsets.all(2), + width: 26, + icon: const FlowySvg(name: 'editor/add'), + onPressed: () { + onEditing(true); + popover.show(); + }, + ); + }, + onSelected: (action, popover) { + onEditing(false); + if (action is ViewAddButtonActionWrapper) { + _showViewAddButtonActions(context, action); + } else if (action is ViewImportActionWrapper) { + _showViewImportAction(context, action); + } + popover.close(); + }, + onClosed: () { + onEditing(false); + }, + ); + } + + void _showViewAddButtonActions( + BuildContext context, + ViewAddButtonActionWrapper action, + ) { + onSelected(action.pluginBuilder, null, null, true); + } + + void _showViewImportAction( + BuildContext context, + ViewImportActionWrapper action, + ) { + showImportPanel( + parentViewId, + context, + (type, name, initialDataBytes) { + if (initialDataBytes == null) { + return; + } + switch (type) { + case ImportType.historyDocument: + case ImportType.historyDatabase: + case ImportType.databaseCSV: + case ImportType.databaseRawData: + onSelected( + action.pluginBuilder, + name, + initialDataBytes, + false, + ); + break; + case ImportType.markdownOrText: + onSelected( + action.pluginBuilder, + name, + initialDataBytes, + true, + ); + break; + } + }, + ); + } +} + +class ViewAddButtonActionWrapper extends ActionCell { + ViewAddButtonActionWrapper({ + required this.pluginBuilder, + }); + + final PluginBuilder pluginBuilder; + + @override + Widget? leftIcon(Color iconColor) => FlowySvg(name: pluginBuilder.menuIcon); + + @override + String get name => pluginBuilder.menuName; + + PluginType get pluginType => pluginBuilder.pluginType; +} + +class ViewImportActionWrapper extends ActionCell { + ViewImportActionWrapper({ + required this.pluginBuilder, + }); + + final DocumentPluginBuilder pluginBuilder; + + @override + Widget? leftIcon(Color iconColor) => const FlowySvg(name: 'editor/import'); + + @override + String get name => LocaleKeys.moreAction_import.tr(); +} 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 new file mode 100644 index 0000000000000..64000ece52d69 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -0,0 +1,304 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +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:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ViewItem extends StatelessWidget { + const ViewItem({ + super.key, + required this.view, + required this.level, + this.leftPadding = 10, + required this.onSelected, + this.isFirstChild = false, + this.isDraggable = true, + }); + + final ViewPB view; + + // indicate the level of the view item + // used to calculate the left padding + final int level; + + // the left padding of the view item for each level + // the left padding of the each level = level * leftPadding + final double leftPadding; + + final void Function(ViewPB) onSelected; + + // used for indicating the first child of the parent view, so that we can + // add top border to the first child + final bool isFirstChild; + + // it should be false when it's rendered as feedback widget inside DraggableItem + final bool isDraggable; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + view.childViews + ..clear() + ..addAll(state.childViews); + return InnerViewItem( + view: view, + level: level, + leftPadding: leftPadding, + showActions: state.isEditing, + isExpanded: state.isExpanded, + onSelected: onSelected, + isFirstChild: isFirstChild, + isDraggable: isDraggable, + ); + }, + ), + ); + } +} + +class InnerViewItem extends StatelessWidget { + const InnerViewItem({ + super.key, + required this.view, + this.isDraggable = true, + this.isExpanded = true, + required this.level, + this.leftPadding = 10, + required this.showActions, + required this.onSelected, + this.isFirstChild = false, + }); + + final ViewPB view; + + final bool isDraggable; + final bool isExpanded; + final bool isFirstChild; + + final int level; + final double leftPadding; + + final bool showActions; + final void Function(ViewPB) onSelected; + + @override + Widget build(BuildContext context) { + Widget child = SingleInnerViewItem( + view: view, + level: level, + showActions: showActions, + onSelected: onSelected, + isExpanded: isExpanded, + ); + + // if the view is expanded and has child views, render its child views + final childViews = view.childViews; + if (isExpanded && childViews.isNotEmpty) { + final children = childViews.map((childView) { + return ViewItem( + key: ValueKey(childView.id), + isFirstChild: childView.id == childViews.first.id, + view: childView, + level: level + 1, + onSelected: onSelected, + isDraggable: isDraggable, + ); + }).toList(); + + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ...children, + ], + ); + } + + // wrap the child with DraggableItem if isDraggable is true + if (isDraggable) { + child = DraggableViewItem( + isFirstChild: isFirstChild, + view: view, + child: child, + feedback: (context) { + return ViewItem( + view: view, + level: level, + onSelected: onSelected, + isDraggable: false, + ); + }, + ); + } + + return child; + } +} + +class SingleInnerViewItem extends StatefulWidget { + const SingleInnerViewItem({ + super.key, + required this.view, + required this.isExpanded, + required this.level, + this.leftPadding = 10, + required this.showActions, + required this.onSelected, + }); + + final ViewPB view; + final bool isExpanded; + + final int level; + final double leftPadding; + + final bool showActions; + final void Function(ViewPB) onSelected; + + @override + State createState() => _SingleInnerViewItemState(); +} + +class _SingleInnerViewItemState extends State { + bool onHover = false; + + @override + Widget build(BuildContext context) { + final children = [ + // expand icon + _buildExpandedIcon(), + const HSpace(7), + // icon + SizedBox.square( + dimension: 16, + child: widget.view.icon(), + ), + const HSpace(5), + // title + Expanded( + child: FlowyText.regular( + widget.view.name, + overflow: TextOverflow.ellipsis, + ), + ) + ]; + + // hover action + if (widget.showActions || onHover) { + // ··· more action button + children.add(_buildViewMoreActionButton(context)); + // + button + children.add(_buildViewAddButton(context)); + } + + return MouseRegion( + onEnter: (_) => setState(() => onHover = true), + onExit: (_) => setState(() => onHover = false), + child: GestureDetector( + onTap: () => widget.onSelected(widget.view), + child: SizedBox( + height: 26, + child: Padding( + padding: EdgeInsets.only(left: widget.level * widget.leftPadding), + child: Row( + children: children, + ), + ), + ), + ), + ); + } + + // > button + Widget _buildExpandedIcon() { + final name = + widget.isExpanded ? 'home/drop_down_show' : 'home/drop_down_hide'; + return GestureDetector( + child: FlowySvg( + name: name, + size: const Size.square(16.0), + ), + onTap: () => context + .read() + .add(ViewEvent.setIsExpanded(!widget.isExpanded)), + ); + } + + // + button + Widget _buildViewAddButton(BuildContext context) { + return Tooltip( + message: LocaleKeys.menuAppHeader_addPageTooltip.tr(), + child: ViewAddButton( + parentViewId: widget.view.id, + onEditing: (value) => + context.read().add(ViewEvent.setIsEditing(value)), + onSelected: (pluginBuilder, name, initialDataBytes, openAfterCreated) { + context.read().add( + ViewEvent.createView( + name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + pluginBuilder.layoutType!, + openAfterCreated: openAfterCreated, + ), + ); + context.read().add( + const ViewEvent.setIsExpanded(true), + ); + }, + ), + ); + } + + // ··· more action button + Widget _buildViewMoreActionButton(BuildContext context) { + return Tooltip( + message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), + child: ViewMoreActionButton( + onEditing: (value) => + context.read().add(ViewEvent.setIsEditing(value)), + onAction: (action) { + switch (action) { + case ViewMoreActionType.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + autoSelectAllText: true, + value: widget.view.name, + confirm: (newValue) { + context.read().add(ViewEvent.rename(newValue)); + }, + ).show(context); + break; + case ViewMoreActionType.delete: + context.read().add(const ViewEvent.delete()); + break; + case ViewMoreActionType.duplicate: + context.read().add(const ViewEvent.duplicate()); + break; + case ViewMoreActionType.openInNewTab: + context.read().add( + TabsEvent.openTab( + plugin: widget.view.plugin(), + view: widget.view, + ), + ); + break; + default: + throw UnsupportedError('$action is not supported'); + } + }, + ), + ); + } +} 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 new file mode 100644 index 0000000000000..da2b367011dd1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_infra/image.dart'; + +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; + +const supportedActionTypes = [ + ViewMoreActionType.rename, + ViewMoreActionType.delete, + ViewMoreActionType.duplicate, + ViewMoreActionType.openInNewTab, +]; + +/// ··· button beside the view name +class ViewMoreActionButton extends StatelessWidget { + const ViewMoreActionButton({ + super.key, + required this.onEditing, + required this.onAction, + }); + + final void Function(bool value) onEditing; + final void Function(ViewMoreActionType) onAction; + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 8), + actions: supportedActionTypes + .map((e) => ViewMoreActionTypeWrapper(e)) + .toList(), + buildChild: (popover) { + return FlowyIconButton( + hoverColor: Colors.transparent, + iconPadding: const EdgeInsets.all(2), + width: 26, + icon: const FlowySvg(name: 'editor/details'), + onPressed: () { + onEditing(true); + popover.show(); + }, + ); + }, + onSelected: (action, popover) { + onEditing(false); + onAction(action.inner); + popover.close(); + }, + onClosed: () => onEditing(false), + ); + } +} + +class ViewMoreActionTypeWrapper extends ActionCell { + ViewMoreActionTypeWrapper(this.inner); + + final ViewMoreActionType inner; + + @override + Widget? leftIcon(Color iconColor) => inner.icon(iconColor); + + @override + String get name => inner.name; +} 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 new file mode 100644 index 0000000000000..9538c2e89ce11 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +class DraggableItem extends StatefulWidget { + const DraggableItem({ + super.key, + required this.child, + required this.data, + this.feedback, + this.childWhenDragging, + this.onAccept, + this.onWillAccept, + this.onMove, + this.onLeave, + this.enableAutoScroll = true, + this.hitTestSize = const Size(100, 100), + }); + + final T data; + + final Widget child; + final Widget? feedback; + final Widget? childWhenDragging; + + final DragTargetAccept? onAccept; + final DragTargetWillAccept? onWillAccept; + final DragTargetMove? onMove; + final DragTargetLeave? onLeave; + + /// Whether to enable auto scroll when dragging. + /// + /// If true, the draggable item must be wrapped inside a [Scrollable] widget. + final bool enableAutoScroll; + final Size hitTestSize; + + @override + State> createState() => _DraggableItemState(); +} + +class _DraggableItemState extends State> { + ScrollableState? scrollable; + EdgeDraggingAutoScroller? autoScroller; + Rect? dragTarget; + + @override + Widget build(BuildContext context) { + initAutoScrollerIfNeeded(context); + + return DragTarget( + onAccept: widget.onAccept, + onWillAccept: widget.onWillAccept, + onMove: widget.onMove, + onLeave: widget.onLeave, + builder: (_, __, ___) => Draggable( + data: widget.data, + feedback: widget.feedback ?? widget.child, + childWhenDragging: widget.childWhenDragging ?? widget.child, + child: widget.child, + onDragUpdate: (details) { + if (widget.enableAutoScroll) { + dragTarget = details.globalPosition & widget.hitTestSize; + autoScroller?.startAutoScrollIfNecessary(dragTarget!); + } + }, + onDragEnd: (details) { + dragTarget = null; + }, + onDraggableCanceled: (_, __) { + dragTarget = null; + }, + ), + ); + } + + void initAutoScrollerIfNeeded(BuildContext context) { + if (!widget.enableAutoScroll) { + return; + } + + scrollable = Scrollable.of(context); + if (scrollable == null) { + throw FlutterError( + 'DraggableItem must be wrapped inside a Scrollable widget ' + 'when enableAutoScroll is true.', + ); + } + autoScroller = EdgeDraggingAutoScroller( + scrollable!, + onScrollViewScrolled: () { + if (dragTarget != null) { + autoScroller!.startAutoScrollIfNecessary(dragTarget!); + } + }, + velocityScalar: 20, + ); + } +} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart index 04ee12d4f31af..26cee376bbca6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart @@ -24,3 +24,28 @@ extension FlowyStyledWidget on Widget { ); } } + +class TopBorder extends StatelessWidget { + const TopBorder({ + super.key, + this.width = 1.0, + this.color = Colors.grey, + required this.child, + }); + + final Widget child; + final double width; + final Color color; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: BorderSide(width: width, color: color), + ), + ), + child: child, + ); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 279ca8f1b4467..edecb1c6feaa3 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -70,7 +70,10 @@ "rename": "Rename", "delete": "Delete", "duplicate": "Duplicate", - "openNewTab": "Open in a new tab" + "openNewTab": "Open in a new tab", + "moveTo": "Move to", + "addToFavorites": "Add to Favorites", + "copyLink": "Copy Link" }, "blankPageTitle": "Blank page", "newPageText": "New page", @@ -111,6 +114,7 @@ "feedback": "Feedback" }, "menuAppHeader": { + "moreButtonToolTip": "Remove, rename, and more...", "addPageTooltip": "Quickly add a page inside", "defaultNewPageName": "Untitled", "renameDialog": "Rename" From ec444e99fecbe661bfac6257626af2eecb5a14c8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 30 Jul 2023 10:12:19 +0800 Subject: [PATCH 02/10] feat: add trash button to sidebar --- .../application/view/view_service.dart | 5 +- .../presentation/home/home_screen.dart | 9 +-- .../home/menu/app/section/section.dart | 2 +- .../home/menu/sidebar/sidebar.dart | 4 ++ .../home/menu/sidebar/sidebar_trash.dart | 60 +++++++++++++++++++ .../home/menu/view/draggable_view_item.dart | 3 - .../draggable_item/draggable_item.dart | 2 + 7 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 0f59052da07a3..295b9453e7819 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -155,7 +155,10 @@ class ViewBackendService { return FolderEventMoveView(payload).send(); } - /// support nested view + /// Move the view to the new parent view. + /// + /// supports nested view + /// if the [prevViewId] is null, the view will be moved to the beginning of the list static Future> moveViewV2({ required String viewId, required String newParentId, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart index c5fbb37edde6a..6e02a28def007 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart @@ -118,7 +118,7 @@ class _HomeScreenState extends State { buildContext: context, ), ); - final menu = _buildHomeMenu( + final menu = _buildHomeSidebar( layout: layout, context: context, ); @@ -140,20 +140,15 @@ class _HomeScreenState extends State { ); } - Widget _buildHomeMenu({ + Widget _buildHomeSidebar({ required HomeLayout layout, required BuildContext context, }) { final workspaceSetting = widget.workspaceSetting; - // final homeMenu = HomeMenu( - // user: widget.user, - // workspaceSetting: workspaceSetting, - // ); final homeMenu = HomeSideBar( user: widget.user, workspaceSetting: workspaceSetting, ); - return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart index 7c6216486b01c..e9136993cf1c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart @@ -43,7 +43,7 @@ class ViewSection extends StatelessWidget { ); } - Widget _reorderableColumn( + ReorderableColumn _reorderableColumn( BuildContext context, ViewSectionState state, ) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 7f089e567a2e8..c98396b69566d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' @@ -77,6 +78,9 @@ class HomeSideBar extends StatelessWidget { ), ), const VSpace(10), + // trash + const SidebarTrashButton(), + const VSpace(24), // new page button const SidebarNewAppButton(), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart new file mode 100644 index 0000000000000..25581af3565f5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart @@ -0,0 +1,60 @@ +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +class SidebarTrashButton extends StatelessWidget { + const SidebarTrashButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: getIt().notifier, + builder: (context, value, child) { + return FlowyHover( + style: HoverStyle( + hoverColor: AFThemeExtension.of(context).greySelect, + ), + isSelected: () => getIt().latestOpenView == null, + child: SizedBox( + height: 26, + child: InkWell( + onTap: () { + getIt().latestOpenView = null; + getIt().add( + TabsEvent.openPlugin( + plugin: makePlugin(pluginType: PluginType.trash), + ), + ); + }, + child: _buildTextButton(context), + ), + ), + ); + }, + ); + } + + Widget _buildTextButton(BuildContext context) { + return Row( + children: [ + const FlowySvg( + size: Size(16, 16), + name: 'home/trash', + ), + const HSpace(6), + FlowyText.medium(LocaleKeys.trash_text.tr()), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 35b1ba4f99205..1929f9e407455 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -167,9 +167,6 @@ extension on ViewPB { return true; } - print('this = $this'); - print('target = $view'); - return childViews.any((v) => v.containsView(view)); } } 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 9538c2e89ce11..cb56e57808492 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 @@ -62,9 +62,11 @@ class _DraggableItemState extends State> { } }, onDragEnd: (details) { + autoScroller?.stopAutoScroll(); dragTarget = null; }, onDraggableCanceled: (_, __) { + autoScroller?.stopAutoScroll(); dragTarget = null; }, ), From f979692e952c4123aa9f126e0d8b45dce7b3479a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 30 Jul 2023 10:28:54 +0800 Subject: [PATCH 03/10] feat: add hover color to view item --- .../home/menu/view/view_item.dart | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) 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 64000ece52d69..4d46b18a338ed 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 @@ -1,7 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -11,6 +13,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -173,10 +176,21 @@ class SingleInnerViewItem extends StatefulWidget { } class _SingleInnerViewItemState extends State { - bool onHover = false; - @override Widget build(BuildContext context) { + return FlowyHover( + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.secondary, + ), + buildWhenOnHover: () => !widget.showActions, + builder: (_, onHover) => _buildViewItem(onHover), + isSelected: () => + widget.showActions || + getIt().latestOpenView?.id == widget.view.id, + ); + } + + Widget _buildViewItem(bool onHover) { final children = [ // expand icon _buildExpandedIcon(), @@ -204,18 +218,14 @@ class _SingleInnerViewItemState extends State { children.add(_buildViewAddButton(context)); } - return MouseRegion( - onEnter: (_) => setState(() => onHover = true), - onExit: (_) => setState(() => onHover = false), - child: GestureDetector( - onTap: () => widget.onSelected(widget.view), - child: SizedBox( - height: 26, - child: Padding( - padding: EdgeInsets.only(left: widget.level * widget.leftPadding), - child: Row( - children: children, - ), + return GestureDetector( + onTap: () => widget.onSelected(widget.view), + child: SizedBox( + height: 26, + child: Padding( + padding: EdgeInsets.only(left: widget.level * widget.leftPadding), + child: Row( + children: children, ), ), ), From 28b45b737dab7386fbc050c0a92dbcd69df3afd6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 30 Jul 2023 16:32:46 +0800 Subject: [PATCH 04/10] feat: update the built-in workspace view --- .../bloc_test/home_test/view_bloc_test.dart | 28 +++++++++++++++++++ .../src/deps_resolve/folder_deps.rs | 23 ++++++--------- .../flowy-folder2/src/user_default.rs | 9 +----- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index d0da0bfb49224..5f3e36e92e817 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -69,4 +69,32 @@ void main() { assert(appBloc.state.views.isEmpty); }); + + test('create nested view test', () async { + final app = await testContext.createTestApp(); + + final appBloc = AppBloc(view: app); + appBloc + ..add( + const AppEvent.initial(), + ) + ..add( + const AppEvent.createView('Document 1', ViewLayoutPB.Document), + ); + await blocResponseFuture(); + + // create a nested view + const name = 'Document 1 - 1'; + final viewBloc = ViewBloc(view: appBloc.state.views.first); + viewBloc + ..add( + const ViewEvent.initial(), + ) + ..add( + const ViewEvent.createView(name, ViewLayoutPB.Document), + ); + await blocResponseFuture(); + + assert(viewBloc.state.childViews.first.name == name); + }); } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index dd48d663ca592..6a6aeaa0b0829 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -100,23 +100,18 @@ impl FolderOperationHandler for DocumentFolderOperation { FutureResult::new(async move { let mut write_guard = workspace_view_builder.write().await; - // Create a parent view named "⭐️ Getting started". and a child view named "Read me". + // Create a view named "⭐️ Getting started" with built-in README data. // Don't modify this code unless you know what you are doing. write_guard .with_view_builder(|view_builder| async { - view_builder - .with_name("⭐️ Getting started") - .with_child_view_builder(|child_view_builder| async { - let view = child_view_builder.with_name("Read me").build(); - let json_str = include_str!("../../assets/read_me.json"); - let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); - manager - .create_document(&view.parent_view.id, Some(document_pb.into())) - .unwrap(); - view - }) - .await - .build() + let view = view_builder.with_name("⭐️ Getting started").build(); + // create a empty document + let json_str = include_str!("../../assets/read_me.json"); + let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); + manager + .create_document(&view.parent_view.id, Some(document_pb.into())) + .unwrap(); + view }) .await; Ok(()) diff --git a/frontend/rust-lib/flowy-folder2/src/user_default.rs b/frontend/rust-lib/flowy-folder2/src/user_default.rs index 3e411d2cad428..a3d17567aefa0 100644 --- a/frontend/rust-lib/flowy-folder2/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder2/src/user_default.rs @@ -27,14 +27,7 @@ impl DefaultFolderBuilder { let views = workspace_view_builder.write().await.build(); // Safe to unwrap because we have at least one view. check out the DocumentFolderOperation. - let first_view = views - .first() - .unwrap() - .child_views - .first() - .unwrap() - .parent_view - .clone(); + let first_view = views.first().unwrap().parent_view.clone(); let first_level_views = views .iter() From 2a884f3ddf31b2233da6c868430f321c9a2df920 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 30 Jul 2023 23:53:44 +0800 Subject: [PATCH 05/10] test: add integration test --- .../database_calendar_test.dart | 19 +-- .../integration_test/database_cell_test.dart | 30 ++-- .../integration_test/database_field_test.dart | 28 ++-- .../database_filter_test.dart | 2 +- .../database_row_page_test.dart | 31 ++-- .../integration_test/database_row_test.dart | 14 +- .../database_setting_test.dart | 7 +- .../integration_test/database_view_test.dart | 9 +- .../document_create_and_delete_test.dart | 24 +-- .../document/document_with_database_test.dart | 8 +- ...cument_with_inline_math_equation_test.dart | 8 +- .../document_with_inline_page_test.dart | 22 ++- .../document/document_with_link_test.dart | 2 +- .../document_with_outline_block_test.dart | 8 +- .../document_with_toggle_list_test.dart | 10 +- .../document/edit_document_test.dart | 14 +- .../integration_test/import_files_test.dart | 6 +- .../integration_test/share_markdown_test.dart | 14 +- .../sidebar/sidebar_test.dart | 142 ++++++++++++++++++ .../integration_test/tabs_test.dart | 10 +- .../integration_test/util/base.dart | 1 - .../util/common_operations.dart | 108 ++++++++++--- .../util/database_test_op.dart | 7 +- .../integration_test/util/expectation.dart | 65 ++++++-- .../mention/mention_page_block.dart | 6 + .../menu/app/header/import/import_panel.dart | 3 + .../menu/app/header/import/import_type.dart | 1 + .../home/menu/view/view_add_button.dart | 29 +--- .../home/menu/view/view_item.dart | 24 ++- 29 files changed, 451 insertions(+), 201 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart diff --git a/frontend/appflowy_flutter/integration_test/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart index 5d4561925c490..18efc129f5a3f 100644 --- a/frontend/appflowy_flutter/integration_test/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_calendar_test.dart @@ -14,8 +14,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateCalendarButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Calendar); // open setting await tester.tapDatabaseSettingButton(); @@ -36,7 +35,11 @@ void main() { await tester.tapGoButton(); // Create calendar view - await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar'); + const name = 'calendar'; + await tester.createNewPageWithName( + name: name, + layout: ViewLayoutPB.Calendar, + ); // Open setting await tester.tapDatabaseSettingButton(); @@ -47,9 +50,9 @@ void main() { await tester.tapFirstDayOfWeekStartFromMonday(); // Open the other page and open the new calendar page again - await tester.openPage(readme); + await tester.openPage(gettingStated); await tester.pumpAndSettle(const Duration(milliseconds: 300)); - await tester.openPage('calendar'); + await tester.openPage(name, layout: ViewLayoutPB.Calendar); // Open setting again and check the start from Monday is selected await tester.tapDatabaseSettingButton(); @@ -65,8 +68,7 @@ void main() { await tester.tapGoButton(); // Create the calendar view - await tester.tapAddButton(); - await tester.tapCreateCalendarButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Calendar); // Scroll until today's date cell is visible await tester.scrollToToday(); @@ -135,8 +137,7 @@ void main() { await tester.tapGoButton(); // Create the calendar view - await tester.tapAddButton(); - await tester.tapCreateCalendarButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Calendar); // Create a new event on the first of this month final today = DateTime.now(); diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart index 27b19ec4ef7c9..b4afef45f0853 100644 --- a/frontend/appflowy_flutter/integration_test/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_cell_test.dart @@ -15,8 +15,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.editCell( rowIndex: 0, @@ -38,7 +37,10 @@ void main() { testWidgets('edit multiple text cells', (tester) async { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid'); + await tester.createNewPageWithName( + name: 'my grid', + layout: ViewLayoutPB.Grid, + ); await tester.createField(FieldType.RichText, 'description'); await tester.editCell( @@ -75,8 +77,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); const fieldType = FieldType.Number; @@ -134,8 +135,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.assertCheckboxCell(rowIndex: 0, isSelected: false); await tester.tapCheckboxCellInGrid(rowIndex: 0); @@ -153,8 +153,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); const fieldType = FieldType.CreatedTime; // Create a create time field @@ -172,8 +171,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); const fieldType = FieldType.LastEditedTime; // Create a last time field @@ -191,8 +189,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); const fieldType = FieldType.DateTime; await tester.createField(fieldType, fieldType.name); @@ -276,9 +273,9 @@ void main() { await tester.tapGoButton(); const fieldType = FieldType.SingleSelect; - await tester.tapAddButton(); + // When create a grid, it will create a single select field by default - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Tap the cell to invoke the selection option editor await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -354,8 +351,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); const fieldType = FieldType.MultiSelect; await tester.createField(fieldType, fieldType.name); diff --git a/frontend/appflowy_flutter/integration_test/database_field_test.dart b/frontend/appflowy_flutter/integration_test/database_field_test.dart index a4bbe35f4e20f..94a382332a792 100644 --- a/frontend/appflowy_flutter/integration_test/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_field_test.dart @@ -17,8 +17,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Invoke the field editor await tester.tapGridFieldWithName('Name'); @@ -35,8 +34,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Invoke the field editor await tester.tapGridFieldWithName('Type'); @@ -58,8 +56,7 @@ void main() { await tester.tapGoButton(); // create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // create a field await tester.createField(FieldType.Checklist, 'checklist'); @@ -73,8 +70,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // create a field await tester.createField(FieldType.Checkbox, 'New field 1'); @@ -94,8 +90,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // create a field await tester.scrollToRight(find.byType(GridPage)); @@ -115,8 +110,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // create a field await tester.scrollToRight(find.byType(GridPage)); @@ -136,8 +130,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.scrollToRight(find.byType(GridPage)); await tester.tapNewPropertyButton(); @@ -157,8 +150,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); for (final fieldType in [ FieldType.Checklist, @@ -190,7 +182,9 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.createNewPageWithName(ViewLayoutPB.Grid); + await tester.createNewPageWithName( + layout: ViewLayoutPB.Grid, + ); // Invoke the field editor await tester.tapGridFieldWithName('Type'); diff --git a/frontend/appflowy_flutter/integration_test/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/database_filter_test.dart index 1ae748801675f..dcae286a7719e 100644 --- a/frontend/appflowy_flutter/integration_test/database_filter_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_filter_test.dart @@ -9,7 +9,7 @@ import 'util/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('grid', () { + group('database filter', () { testWidgets('add text filter', (tester) async { await tester.openV020database(); diff --git a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart index 4c706ae443181..373ef7b0de216 100644 --- a/frontend/appflowy_flutter/integration_test/database_row_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_row_page_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -19,8 +20,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -34,8 +34,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -55,8 +54,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -85,8 +83,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -108,8 +105,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -144,8 +140,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -160,8 +155,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -201,8 +195,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -241,8 +234,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); @@ -258,8 +250,7 @@ void main() { await tester.tapGoButton(); // Create a new grid - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Hover first row and then open the row page await tester.openFirstRowDetailPage(); diff --git a/frontend/appflowy_flutter/integration_test/database_row_test.dart b/frontend/appflowy_flutter/integration_test/database_row_test.dart index c3e48823ac42a..08225a42cd4cc 100644 --- a/frontend/appflowy_flutter/integration_test/database_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_row_test.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -12,8 +13,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.tapCreateRowButtonInGrid(); // The initial number of rows is 3 @@ -25,8 +25,8 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); + await tester.hoverOnFirstRowOfGrid(); await tester.tapCreateRowButtonInRowMenuOfGrid(); @@ -41,8 +41,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.hoverOnFirstRowOfGrid(); // Open the row menu and then click the delete @@ -60,8 +59,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); await tester.assertRowCountInGridPage(3); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/integration_test/database_setting_test.dart b/frontend/appflowy_flutter/integration_test/database_setting_test.dart index 6b0b7fe992e1f..84fe9c1b39cec 100644 --- a/frontend/appflowy_flutter/integration_test/database_setting_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_setting_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -13,8 +14,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // open setting await tester.tapDatabaseSettingButton(); @@ -31,8 +31,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // open setting await tester.tapDatabaseSettingButton(); diff --git a/frontend/appflowy_flutter/integration_test/database_view_test.dart b/frontend/appflowy_flutter/integration_test/database_view_test.dart index 99ccd25b97813..88c04eb45c52f 100644 --- a/frontend/appflowy_flutter/integration_test/database_view_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_view_test.dart @@ -15,8 +15,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Create board view await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); @@ -37,8 +36,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Create board view await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); @@ -63,8 +61,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - await tester.tapAddButton(); - await tester.tapCreateGridButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Grid); // Create board view await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board); diff --git a/frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart b/frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart index c4474949047e2..922207c2ae4fb 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart @@ -17,9 +17,7 @@ void main() { await tester.tapGoButton(); // create a new document - await tester.tapAddButton(); - await tester.tapCreateDocumentButton(); - await tester.pumpAndSettle(); + await tester.createNewPageWithName(); // expect to see a new document tester.expectToSeePageName( @@ -35,19 +33,21 @@ void main() { await tester.tapGoButton(); // delete the readme page - await tester.hoverOnPageName(readme); - await tester.tapDeletePageButton(); + await tester.hoverOnPageName( + gettingStated, + onHover: () async => await tester.tapDeletePageButton(), + ); // the banner should show up and the readme page should be gone tester.expectToSeeDocumentBanner(); - tester.expectNotToSeePageName(readme); + tester.expectNotToSeePageName(gettingStated); // restore the readme page await tester.tapRestoreButton(); // the banner should be gone and the readme page should be back tester.expectNotToSeeDocumentBanner(); - tester.expectToSeePageName(readme); + tester.expectToSeePageName(gettingStated); }); testWidgets('delete the readme page and delete it permanently', @@ -57,19 +57,21 @@ void main() { await tester.tapGoButton(); // delete the readme page - await tester.hoverOnPageName(readme); - await tester.tapDeletePageButton(); + await tester.hoverOnPageName( + gettingStated, + onHover: () async => await tester.tapDeletePageButton(), + ); // the banner should show up and the readme page should be gone tester.expectToSeeDocumentBanner(); - tester.expectNotToSeePageName(readme); + tester.expectNotToSeePageName(gettingStated); // delete the page permanently await tester.tapDeletePermanentlyButton(); // the banner should be gone and the readme page should be gone tester.expectNotToSeeDocumentBanner(); - tester.expectNotToSeePageName(readme); + tester.expectNotToSeePageName(gettingStated); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart index b121a8db59abe..46a3dcd296c1d 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart @@ -73,13 +73,13 @@ Future insertReferenceDatabase( final id = uuid(); final name = '${layout.name}_$id'; await tester.createNewPageWithName( - layout, - name, + name: name, + layout: layout, ); // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, - 'insert_a_reference_${layout.name}', + name: 'insert_a_reference_${layout.name}', + layout: ViewLayoutPB.Document, ); // tap the first line of the document await tester.editor.tapLineOfEditorAt(0); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart index 2a04279ca136f..f6d787c8fbae3 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart @@ -21,8 +21,8 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, - LocaleKeys.document_plugins_createInlineMathEquation.tr(), + name: LocaleKeys.document_plugins_createInlineMathEquation.tr(), + layout: ViewLayoutPB.Document, ); // tap the first line of the document @@ -67,8 +67,8 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, - LocaleKeys.document_plugins_createInlineMathEquation.tr(), + name: LocaleKeys.document_plugins_createInlineMathEquation.tr(), + layout: ViewLayoutPB.Document, ); // tap the first line of the document diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart index 437f165d0c278..e071f3ee363aa 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart @@ -62,9 +62,11 @@ void main() { final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document); // rename - await tester.hoverOnPageName(pageName); const newName = 'RenameToNewPageName'; - await tester.renamePage(newName); + await tester.hoverOnPageName( + pageName, + onHover: () async => await tester.renamePage(newName), + ); final finder = find.descendant( of: find.byType(MentionPageBlock), matching: find.findTextInFlowyText(newName), @@ -79,8 +81,11 @@ void main() { final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid); // rename - await tester.hoverOnPageName(pageName); - await tester.tapDeletePageButton(); + await tester.hoverOnPageName( + pageName, + layout: ViewLayoutPB.Grid, + onHover: () async => await tester.tapDeletePageButton(), + ); final finder = find.descendant( of: find.byType(MentionPageBlock), matching: find.findTextInFlowyText(pageName), @@ -101,13 +106,14 @@ Future insertingInlinePage( final id = uuid(); final name = '${layout.name}_$id'; await tester.createNewPageWithName( - layout, - name, + name: name, + layout: layout, + openAfterCreated: false, ); // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, - 'insert_a_inline_page_${layout.name}', + name: 'insert_a_inline_page_${layout.name}', + layout: ViewLayoutPB.Document, ); // tap the first line of the document await tester.editor.tapLineOfEditorAt(0); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart index e7500f364a8d3..64d66e885cc93 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart @@ -25,7 +25,7 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, + layout: ViewLayoutPB.Document, ); // tap the first line of the document diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart index 94bbb5ae3a151..60673e4b84263 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart @@ -16,8 +16,8 @@ void main() { await tester.tapGoButton(); await tester.createNewPageWithName( - ViewLayoutPB.Document, - 'outline_test', + name: 'outline_test', + layout: ViewLayoutPB.Document, ); await tester.editor.tapLineOfEditorAt(0); @@ -33,8 +33,8 @@ void main() { await tester.tapGoButton(); await tester.createNewPageWithName( - ViewLayoutPB.Document, - 'outline_test', + name: 'outline_test', + layout: ViewLayoutPB.Document, ); await tester.editor.tapLineOfEditorAt(0); diff --git a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart b/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart index 65f46daa869c3..58ac0c0fb6dc5 100644 --- a/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart @@ -32,7 +32,7 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, + layout: ViewLayoutPB.Document, ); // tap the first line of the document @@ -78,7 +78,7 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, + layout: ViewLayoutPB.Document, ); // tap the first line of the document @@ -118,7 +118,7 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, + layout: ViewLayoutPB.Document, ); // tap the first line of the document @@ -156,7 +156,7 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, + layout: ViewLayoutPB.Document, ); // tap the first line of the document @@ -191,7 +191,7 @@ void main() { // create a new document await tester.createNewPageWithName( - ViewLayoutPB.Document, + layout: ViewLayoutPB.Document, ); // tap the first line of the document diff --git a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart index 9cbe5433e7840..7050778b3aece 100644 --- a/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart +++ b/frontend/appflowy_flutter/integration_test/document/edit_document_test.dart @@ -18,7 +18,10 @@ void main() { // create a new document called Sample const pageName = 'Sample'; - await tester.createNewPageWithName(ViewLayoutPB.Document, pageName); + await tester.createNewPageWithName( + name: pageName, + layout: ViewLayoutPB.Document, + ); // focus on the editor await tester.editor.tapLineOfEditorAt(0); @@ -56,7 +59,7 @@ void main() { ); // switch to other page and switch back - await tester.openPage(readme); + await tester.openPage(gettingStated); await tester.openPage(pageName); // the numbered list should be kept @@ -72,7 +75,10 @@ void main() { // create a new document called Sample const pageName = 'Sample'; - await tester.createNewPageWithName(ViewLayoutPB.Document, pageName); + await tester.createNewPageWithName( + name: pageName, + layout: ViewLayoutPB.Document, + ); // focus on the editor await tester.editor.tapLineOfEditorAt(0); @@ -85,7 +91,7 @@ void main() { } // switch to other page and switch back - await tester.openPage(readme); + await tester.openPage(gettingStated); await tester.openPage(pageName); // this screenshots are different on different platform, so comment it out temporarily. diff --git a/frontend/appflowy_flutter/integration_test/import_files_test.dart b/frontend/appflowy_flutter/integration_test/import_files_test.dart index ce302ba53dee1..27e448474c08f 100644 --- a/frontend/appflowy_flutter/integration_test/import_files_test.dart +++ b/frontend/appflowy_flutter/integration_test/import_files_test.dart @@ -16,10 +16,10 @@ void main() { final context = await tester.initializeAppFlowy(); await tester.tapGoButton(); - // expect to see a readme page - tester.expectToSeePageName(readme); + // expect to see a getting started page + tester.expectToSeePageName(gettingStated); - await tester.tapAddButton(); + await tester.tapAddViewButton(); await tester.tapImportButton(); final testFileNames = ['test1.md', 'test2.md']; diff --git a/frontend/appflowy_flutter/integration_test/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/share_markdown_test.dart index 55ec5b890c4cb..97426aa3c1f45 100644 --- a/frontend/appflowy_flutter/integration_test/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/share_markdown_test.dart @@ -16,7 +16,7 @@ void main() { await tester.tapGoButton(); // expect to see a readme page - tester.expectToSeePageName(readme); + tester.expectToSeePageName(gettingStated); // mock the file picker final path = await mockSaveFilePath( @@ -42,12 +42,16 @@ void main() { final context = await tester.initializeAppFlowy(); await tester.tapGoButton(); - // expect to see a readme page - tester.expectToSeePageName(readme); + // expect to see a getting started page + tester.expectToSeePageName(gettingStated); // rename the document - await tester.hoverOnPageName(readme); - await tester.renamePage('example'); + await tester.hoverOnPageName( + gettingStated, + onHover: () async { + await tester.renamePage('example'); + }, + ); final shareButton = find.byType(ShareActionList); final shareButtonState = diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart new file mode 100644 index 0000000000000..19c1484248065 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart @@ -0,0 +1,142 @@ +import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('sidebar test', () { + testWidgets('create a new page', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // create a new page + const name = 'Hello AppFlowy'; + await tester.tapNewPageButton(); + await tester.enterText(find.byType(TextFormField), name); + await tester.tapOKButton(); + + // expect to see a new document + tester.expectToSeePageName( + name, + ); + // and with one paragraph block + expect(find.byType(TextBlockComponentWidget), findsOneWidget); + }); + + testWidgets('create a new document, grid, board and calendar', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + for (final layout in ViewLayoutPB.values) { + // create a new page + final name = 'AppFlowy_$layout'; + await tester.createNewPageWithName( + name: name, + layout: layout, + ); + + // expect to see a new page + tester.expectToSeePageName( + name, + layout: layout, + ); + + switch (layout) { + case ViewLayoutPB.Document: + // and with one paragraph block + expect(find.byType(TextBlockComponentWidget), findsOneWidget); + break; + case ViewLayoutPB.Grid: + expect(find.byType(GridPage), findsOneWidget); + break; + case ViewLayoutPB.Board: + expect(find.byType(BoardPage), findsOneWidget); + break; + case ViewLayoutPB.Calendar: + expect(find.byType(CalendarPage), findsOneWidget); + break; + } + + await tester.openPage(gettingStated); + } + }); + + testWidgets('create some nested pages, and move them', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final names = [1, 2, 3, 4].map((e) => 'document_$e').toList(); + for (var i = 0; i < names.length; i++) { + final parentName = i == 0 ? gettingStated : names[i - 1]; + await tester.createNewPageWithName( + name: names[i], + parentName: parentName, + layout: ViewLayoutPB.Document, + ); + tester.expectToSeePageName(names[i], parentName: parentName); + } + + // move the document_3 to the getting started page + await tester.movePageToOtherPage( + name: names[3], + parentName: gettingStated, + layout: ViewLayoutPB.Document, + parentLayout: ViewLayoutPB.Document, + ); + final fromId = tester + .widget(tester.findPageName(names[3])) + .view + .parentViewId; + final toId = tester + .widget(tester.findPageName(gettingStated)) + .view + .id; + expect(fromId, toId); + + // move the document_2 before document_1 + await tester.movePageToOtherPage( + name: names[2], + parentName: gettingStated, + layout: ViewLayoutPB.Document, + parentLayout: ViewLayoutPB.Document, + position: DraggableHoverPosition.bottom, + ); + final childViews = tester + .widget(tester.findPageName(gettingStated)) + .view + .childViews; + expect( + childViews[0].id, + tester + .widget(tester.findPageName(names[2])) + .view + .id, + ); + expect( + childViews[1].id, + tester + .widget(tester.findPageName(names[0])) + .view + .id, + ); + expect( + childViews[2].id, + tester + .widget(tester.findPageName(names[3])) + .view + .id, + ); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/tabs_test.dart b/frontend/appflowy_flutter/integration_test/tabs_test.dart index 6f8249c3de326..333f9a5f2977d 100644 --- a/frontend/appflowy_flutter/integration_test/tabs_test.dart +++ b/frontend/appflowy_flutter/integration_test/tabs_test.dart @@ -29,8 +29,14 @@ void main() { findsNothing, ); - await tester.createNewPageWithName(ViewLayoutPB.Calendar, _calendarName); - await tester.createNewPageWithName(ViewLayoutPB.Document, _documentName); + await tester.createNewPageWithName( + name: _calendarName, + layout: ViewLayoutPB.Calendar, + ); + await tester.createNewPageWithName( + name: _documentName, + layout: ViewLayoutPB.Document, + ); // Navigate current view to "Read me" document again await tester.tapButtonWithName(_readmeName); diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index 9a6a199b21feb..27ca2d529647b 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -91,7 +91,6 @@ extension AppFlowyTestBase on WidgetTester { warnIfMissed: warnIfMissed, ); await pumpAndSettle(Duration(milliseconds: milliseconds)); - return; } Future tapButtonWithName( diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 3c65d9085497f..b9c100a8b2bbf 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -1,10 +1,14 @@ import 'dart:ui'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +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_backend/log.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; -import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -24,35 +28,48 @@ extension CommonOperations on WidgetTester { } /// Tap the + button on the home page. - Future tapAddButton() async { - final addButton = find.byType(AddButton); - await tapButton(addButton); + Future tapAddViewButton({ + String name = gettingStated, + }) async { + await hoverOnPageName( + name, + onHover: () async { + final addButton = find.byType(ViewAddButton); + await tapButton(addButton); + }, + ); + } + + /// Tap the 'New Page' Button on the sidebar. + Future tapNewPageButton() async { + final newPageButton = find.byType(SidebarNewAppButton); + await tapButton(newPageButton); } /// Tap the create document button. /// - /// Must call [tapAddButton] first. + /// Must call [tapAddViewButton] first. Future tapCreateDocumentButton() async { await tapButtonWithName(LocaleKeys.document_menuName.tr()); } /// Tap the create grid button. /// - /// Must call [tapAddButton] first. + /// Must call [tapAddViewButton] first. Future tapCreateGridButton() async { await tapButtonWithName(LocaleKeys.grid_menuName.tr()); } /// Tap the create grid button. /// - /// Must call [tapAddButton] first. + /// Must call [tapAddViewButton] first. Future tapCreateCalendarButton() async { await tapButtonWithName(LocaleKeys.calendar_menuName.tr()); } /// Tap the import button. /// - /// Must call [tapAddButton] first. + /// Must call [tapAddViewButton] first. Future tapImportButton() async { await tapButtonWithName(LocaleKeys.moreAction_import.tr()); } @@ -116,6 +133,7 @@ extension CommonOperations on WidgetTester { Finder finder, { Offset? offset, Future Function()? onHover, + bool removePointer = true, }) async { try { final gesture = await createGesture(kind: PointerDeviceKind.mouse); @@ -133,19 +151,30 @@ extension CommonOperations on WidgetTester { /// Hover on the page name. Future hoverOnPageName( String name, { + ViewLayoutPB layout = ViewLayoutPB.Document, Future Function()? onHover, bool useLast = true, }) async { + final pageNames = findPageName(name, layout: layout); if (useLast) { - await hoverOnWidget(findPageName(name).last, onHover: onHover); + await hoverOnWidget( + pageNames.last, + onHover: onHover, + ); } else { - await hoverOnWidget(findPageName(name).first, onHover: onHover); + await hoverOnWidget( + pageNames.first, + onHover: onHover, + ); } } /// open the page with given name. - Future openPage(String name) async { - final finder = findPageName(name); + Future openPage( + String name, { + ViewLayoutPB layout = ViewLayoutPB.Document, + }) async { + final finder = findPageName(name, layout: layout); expect(finder, findsOneWidget); await tapButton(finder); } @@ -154,20 +183,20 @@ extension CommonOperations on WidgetTester { /// /// Must call [hoverOnPageName] first. Future tapPageOptionButton() async { - final optionButton = find.byType(ViewDisclosureButton); + final optionButton = find.byType(ViewMoreActionButton); await tapButton(optionButton); } /// Tap the delete page button. Future tapDeletePageButton() async { await tapPageOptionButton(); - await tapButtonWithName(ViewDisclosureAction.delete.name); + await tapButtonWithName(ViewMoreActionType.delete.name); } /// Tap the rename page button. Future tapRenamePageButton() async { await tapPageOptionButton(); - await tapButtonWithName(ViewDisclosureAction.rename.name); + await tapButtonWithName(ViewMoreActionType.rename.name); } /// Rename the page. @@ -224,12 +253,14 @@ extension CommonOperations on WidgetTester { await tapButton(markdownButton); } - Future createNewPageWithName( - ViewLayoutPB layout, [ + Future createNewPageWithName({ String? name, - ]) async { + ViewLayoutPB layout = ViewLayoutPB.Document, + String? parentName, + bool openAfterCreated = true, + }) async { // create a new page - await tapAddButton(); + await tapAddViewButton(name: parentName ?? gettingStated); await tapButtonWithName(layout.menuName); await pumpAndSettle(); @@ -237,6 +268,7 @@ extension CommonOperations on WidgetTester { if (name != null) { await hoverOnPageName( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + layout: layout, onHover: () async { await renamePage(name); await pumpAndSettle(); @@ -244,6 +276,16 @@ extension CommonOperations on WidgetTester { ); await pumpAndSettle(); } + + // open the page after created + if (openAfterCreated) { + await openPage( + // if the name is null, use the default name + name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + layout: layout, + ); + await pumpAndSettle(); + } } Future simulateKeyEvent( @@ -289,6 +331,34 @@ extension CommonOperations on WidgetTester { await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr())); await pumpAndSettle(); } + + Future movePageToOtherPage({ + required String name, + required String parentName, + required ViewLayoutPB layout, + required ViewLayoutPB parentLayout, + DraggableHoverPosition position = DraggableHoverPosition.center, + }) async { + final from = findPageName(name, layout: layout); + final to = findPageName(parentName, layout: parentLayout); + final gesture = await startGesture(getCenter(from)); + Offset offset = Offset.zero; + switch (position) { + case DraggableHoverPosition.center: + offset = getCenter(to); + break; + case DraggableHoverPosition.top: + offset = getTopLeft(to); + break; + case DraggableHoverPosition.bottom: + offset = getBottomLeft(to); + break; + default: + } + await gesture.moveTo(offset); + await gesture.up(); + await pumpAndSettle(); + } } extension ViewLayoutPBTest on ViewLayoutPB { diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 2ac5819642bc7..800f9ae4c38ec 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -77,9 +77,9 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapGoButton(); // expect to see a readme page - expectToSeePageName(readme); + expectToSeePageName(gettingStated); - await tapAddButton(); + await tapAddViewButton(); await tapImportButton(); final testFileNames = ['v020.afdb']; @@ -103,7 +103,8 @@ extension AppFlowyDatabaseTest on WidgetTester { paths: paths, ); await tapDatabaseRawDataButton(); - await openPage('v020'); + await pumpAndSettle(); + await openPage('v020', layout: ViewLayoutPB.Grid); } Future hoverOnFirstRowOfGrid() async { diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index fda463eb6343b..a126410ffccdd 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -3,29 +3,51 @@ import 'package:appflowy/plugins/document/presentation/banner.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_test/flutter_test.dart'; -const String readme = 'Read me'; +// const String readme = 'Read me'; +const String gettingStated = '⭐️ Getting started'; extension Expectation on WidgetTester { /// Expect to see the home page and with a default read me page. void expectToSeeHomePage() { expect(find.byType(HomeStack), findsOneWidget); - expect(find.textContaining(readme), findsWidgets); + expect(find.textContaining(gettingStated), findsWidgets); } /// Expect to see the page name on the home page. - void expectToSeePageName(String name) { - final pageName = findPageName(name); + void expectToSeePageName( + String name, { + String? parentName, + ViewLayoutPB layout = ViewLayoutPB.Document, + ViewLayoutPB parentLayout = ViewLayoutPB.Document, + }) { + final pageName = findPageName( + name, + layout: layout, + parentName: parentName, + parentLayout: parentLayout, + ); expect(pageName, findsOneWidget); } /// Expect not to see the page name on the home page. - void expectNotToSeePageName(String name) { - final pageName = findPageName(name); + void expectNotToSeePageName( + String name, { + String? parentName, + ViewLayoutPB layout = ViewLayoutPB.Document, + ViewLayoutPB parentLayout = ViewLayoutPB.Document, + }) { + final pageName = findPageName( + name, + layout: layout, + parentName: parentName, + parentLayout: parentLayout, + ); expect(pageName, findsNothing); } @@ -126,10 +148,31 @@ extension Expectation on WidgetTester { } /// Find the page name on the home page. - Finder findPageName(String name) { - return find.byWidgetPredicate( - (widget) => widget is ViewSectionItem && widget.view.name == name, - skipOffstage: false, + Finder findPageName( + String name, { + ViewLayoutPB layout = ViewLayoutPB.Document, + String? parentName, + ViewLayoutPB parentLayout = ViewLayoutPB.Document, + }) { + if (parentName == null) { + return find.byWidgetPredicate( + (widget) => + widget is SingleInnerViewItem && + widget.view.name == name && + widget.view.layout == layout, + skipOffstage: false, + ); + } + + return find.descendant( + of: find.byWidgetPredicate( + (widget) => + widget is ViewItem && + widget.view.name == name && + widget.view.layout == layout, + skipOffstage: false, + ), + matching: findPageName(name), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index d0b4609859627..2411fde431b26 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/startup/startup.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'; import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; @@ -109,6 +110,11 @@ class _MentionPageBlockState extends State { return; } getIt().latestOpenView = view; + getIt().add( + TabsEvent.openPlugin( + plugin: view.plugin(), + ), + ); } Future fetchView(String pageId) async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart index 3bb11d291f755..402696714dfd1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart @@ -83,6 +83,7 @@ class ImportPanel extends StatelessWidget { e.toString(), fontSize: 15, overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.tertiary, ), onTap: () async { await _importFile(parentViewId, e); @@ -157,6 +158,8 @@ class ImportPanel extends StatelessWidget { assert(false, 'Unsupported Type $importType'); } } + + importCallback(importType, '', null); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart index 64630199779ac..b07a8fffba96f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart @@ -43,6 +43,7 @@ enum ImportType { } return FlowySvg( name: name, + color: Theme.of(context).colorScheme.tertiary, ); }; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart index 72ad20b8e87ec..2beae5070a31d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart @@ -2,7 +2,6 @@ import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart'; -import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_type.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; @@ -26,6 +25,7 @@ class ViewAddButton extends StatelessWidget { String? name, List? initialDataBytes, bool openAfterCreated, + bool createNewView, ) onSelected; List get _actions { @@ -82,7 +82,7 @@ class ViewAddButton extends StatelessWidget { BuildContext context, ViewAddButtonActionWrapper action, ) { - onSelected(action.pluginBuilder, null, null, true); + onSelected(action.pluginBuilder, null, null, true, true); } void _showViewImportAction( @@ -93,30 +93,7 @@ class ViewAddButton extends StatelessWidget { parentViewId, context, (type, name, initialDataBytes) { - if (initialDataBytes == null) { - return; - } - switch (type) { - case ImportType.historyDocument: - case ImportType.historyDatabase: - case ImportType.databaseCSV: - case ImportType.databaseRawData: - onSelected( - action.pluginBuilder, - name, - initialDataBytes, - false, - ); - break; - case ImportType.markdownOrText: - onSelected( - action.pluginBuilder, - name, - initialDataBytes, - true, - ); - break; - } + onSelected(action.pluginBuilder, null, null, true, false); }, ); } 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 4d46b18a338ed..983e4ccc0ae7f 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 @@ -255,14 +255,22 @@ class _SingleInnerViewItemState extends State { parentViewId: widget.view.id, onEditing: (value) => context.read().add(ViewEvent.setIsEditing(value)), - onSelected: (pluginBuilder, name, initialDataBytes, openAfterCreated) { - context.read().add( - ViewEvent.createView( - name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - pluginBuilder.layoutType!, - openAfterCreated: openAfterCreated, - ), - ); + onSelected: ( + pluginBuilder, + name, + initialDataBytes, + openAfterCreated, + createNewView, + ) { + if (createNewView) { + context.read().add( + ViewEvent.createView( + name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + pluginBuilder.layoutType!, + openAfterCreated: openAfterCreated, + ), + ); + } context.read().add( const ViewEvent.setIsExpanded(true), ); From f8abd4095e585d0b6c4fa6b33d688c2a3f551cfc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 31 Jul 2023 12:10:19 +0800 Subject: [PATCH 06/10] fix: some UI issues --- .../util/common_operations.dart | 2 +- .../workspace/application/view/view_ext.dart | 18 +++++++----------- .../home/menu/sidebar/sidebar.dart | 7 +++++-- .../menu/sidebar/sidebar_new_page_button.dart | 8 ++++---- .../home/menu/sidebar/sidebar_top_menu.dart | 4 +++- .../home/menu/sidebar/sidebar_trash.dart | 1 + 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index b9c100a8b2bbf..a0b6ace78b2ae 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -42,7 +42,7 @@ extension CommonOperations on WidgetTester { /// Tap the 'New Page' Button on the sidebar. Future tapNewPageButton() async { - final newPageButton = find.byType(SidebarNewAppButton); + final newPageButton = find.byType(SidebarNewPageButton); await tapButton(newPageButton); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index c8ed525e322f8..b0be52ae4ddf2 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -45,17 +45,13 @@ extension ViewExtension on ViewPB { } Widget icon() { - String iconName = 'file_icon'; - switch (layout) { - case ViewLayoutPB.Board: - iconName = 'editor/board'; - case ViewLayoutPB.Calendar: - iconName = 'editor/calendar'; - case ViewLayoutPB.Grid: - iconName = 'editor/grid'; - case ViewLayoutPB.Document: - iconName = 'editor/documents'; - } + final iconName = switch (layout) { + ViewLayoutPB.Board => 'editor/board', + ViewLayoutPB.Calendar => 'editor/calendar', + ViewLayoutPB.Grid => 'editor/grid', + ViewLayoutPB.Document => 'editor/documents', + _ => 'file_icon', + }; return FlowySvg( name: iconName, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index c98396b69566d..4fc112cbface2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -80,9 +80,12 @@ class HomeSideBar extends StatelessWidget { const VSpace(10), // trash const SidebarTrashButton(), - const VSpace(24), + const VSpace(10), // new page button - const SidebarNewAppButton(), + const Padding( + padding: EdgeInsets.only(left: 6.0), + child: SidebarNewPageButton(), + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart index 5e4d6827dd69a..8bce5d12ca954 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart @@ -8,8 +8,8 @@ import 'package:flowy_infra_ui/style_widget/extension.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class SidebarNewAppButton extends StatelessWidget { - const SidebarNewAppButton({ +class SidebarNewPageButton extends StatelessWidget { + const SidebarNewPageButton({ super.key, }); @@ -20,7 +20,7 @@ class SidebarNewAppButton extends StatelessWidget { fillColor: Colors.transparent, hoverColor: Colors.transparent, fontColor: Theme.of(context).colorScheme.tertiary, - onPressed: () async => await _showCreateAppDialog(context), + onPressed: () async => await _showCreatePageDialog(context), heading: Container( width: 16, height: 16, @@ -42,7 +42,7 @@ class SidebarNewAppButton extends StatelessWidget { ); } - Future _showCreateAppDialog(BuildContext context) async { + Future _showCreatePageDialog(BuildContext context) async { return NavigatorTextFieldDialog( title: LocaleKeys.newPageText.tr(), value: '', diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart index 30583d76129fd..20d5be7fe8c51 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -49,7 +49,9 @@ class SidebarTopMenu extends StatelessWidget { final name = Theme.of(context).brightness == Brightness.dark ? 'flowy_logo_dark_mode' : 'flowy_logo_with_text'; - return FlowySvg(name: name); + return svgWidget( + name, + ); } Widget _buildCollapseMenuButton(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart index 25581af3565f5..1eacc316c567d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart @@ -48,6 +48,7 @@ class SidebarTrashButton extends StatelessWidget { Widget _buildTextButton(BuildContext context) { return Row( children: [ + const HSpace(6), const FlowySvg( size: Size(16, 16), name: 'home/trash', From 7b19035feb8ce279459aa3b95802b75b9cf2972f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 31 Jul 2023 15:15:59 +0800 Subject: [PATCH 07/10] fix: appflowy logo overflow --- .../presentation/home/menu/sidebar/sidebar_top_menu.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart index 20d5be7fe8c51..3a2493bdba043 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -51,6 +51,7 @@ class SidebarTopMenu extends StatelessWidget { : 'flowy_logo_with_text'; return svgWidget( name, + size: const Size(92, 17), ); } From 168b9af93df78ce806ea8d6327829814f9cd1265 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 31 Jul 2023 15:51:49 +0800 Subject: [PATCH 08/10] feat: add personal header --- .../menu/sidebar/folder/personal_folder.dart | 114 ++++++++++++++++++ .../home/menu/sidebar/sidebar.dart | 2 +- .../home/menu/sidebar/sidebar_folder.dart | 26 +--- .../home/menu/sidebar/sidebar_trash.dart | 4 +- frontend/resources/translations/en.json | 6 +- 5 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart new file mode 100644 index 0000000000000..7b4cc03ec5dcd --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class PersonalFolder extends StatefulWidget { + const PersonalFolder({ + super.key, + required this.views, + }); + + final List views; + + @override + State createState() => _PersonalFolderState(); +} + +class _PersonalFolderState extends State { + bool isExpanded = true; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + PersonalFolderHeader( + onPressed: () => setState( + () => isExpanded = !isExpanded, + ), + onAdded: () => setState(() => isExpanded = true), + ), + if (isExpanded) + ...widget.views.map( + (view) => ViewItem( + key: ValueKey(view.id), + isFirstChild: view.id == widget.views.first.id, + view: view, + level: 0, + onSelected: (view) { + getIt().latestOpenView = view; + context.read().add(MenuEvent.openPage(view.plugin())); + }, + ), + ) + ], + ); + } +} + +class PersonalFolderHeader extends StatefulWidget { + const PersonalFolderHeader({ + super.key, + required this.onPressed, + required this.onAdded, + }); + + final VoidCallback onPressed; + final VoidCallback onAdded; + + @override + State createState() => _PersonalFolderHeaderState(); +} + +class _PersonalFolderHeaderState extends State { + bool onHover = false; + + @override + Widget build(BuildContext context) { + const iconSize = 26.0; + return MouseRegion( + onEnter: (event) => setState(() => onHover = true), + onExit: (event) => setState(() => onHover = false), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlowyTextButton( + LocaleKeys.sideBar_personal.tr(), + tooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), + constraints: const BoxConstraints(maxHeight: iconSize), + padding: const EdgeInsets.all(4), + fillColor: Colors.transparent, + onPressed: widget.onPressed, + ), + if (onHover) ...[ + const Spacer(), + FlowyIconButton( + tooltipText: LocaleKeys.sideBar_addAPage.tr(), + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + iconPadding: const EdgeInsets.all(2), + height: iconSize, + width: iconSize, + icon: const FlowySvg(name: 'editor/add'), + onPressed: () { + context.read().add( + MenuEvent.createApp( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ), + ); + widget.onAdded(); + }, + ), + ] + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 4fc112cbface2..590b10063aeba 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -68,7 +68,7 @@ class HomeSideBar extends StatelessWidget { // user, setting SidebarUser(user: user), // Favorite, Not supported yet - const VSpace(30), + const VSpace(20), // scrollable document list Expanded( child: SingleChildScrollView( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart index 564fbe0b40ec9..2e268312f540d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart @@ -1,11 +1,6 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarFolder extends StatelessWidget { const SidebarFolder({ @@ -18,20 +13,11 @@ class SidebarFolder extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - children: views - .map( - (view) => ViewItem( - key: ValueKey(view.id), - isFirstChild: view.id == views.first.id, - view: view, - level: 0, - onSelected: (view) { - getIt().latestOpenView = view; - context.read().add(MenuEvent.openPage(view.plugin())); - }, - ), - ) - .toList(), + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // personal + PersonalFolder(views: views), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart index 1eacc316c567d..bf517d4da609b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart @@ -54,7 +54,9 @@ class SidebarTrashButton extends StatelessWidget { name: 'home/trash', ), const HSpace(6), - FlowyText.medium(LocaleKeys.trash_text.tr()), + FlowyText.medium( + LocaleKeys.trash_text.tr(), + ), ], ); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 172fcb10f6bdc..83edee46e60e4 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -150,7 +150,11 @@ }, "sideBar": { "closeSidebar": "Close side bar", - "openSidebar": "Open side bar" + "openSidebar": "Open side bar", + "personal": "Personal", + "favorites": "Favorites", + "clickToHidePersonal": "Click to hide personal section", + "addAPage": "Add a page" }, "notifications": { "export": { From 640c9df5531ef71aed9a3cc6e28fe3cfc516eb61 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 31 Jul 2023 16:45:48 +0800 Subject: [PATCH 09/10] fix: auto scroller infinite loop will cause crash --- .../widgets/draggable_item/draggable_item.dart | 9 +++++++++ 1 file changed, 9 insertions(+) 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 cb56e57808492..634f35b2a99f9 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 @@ -41,6 +41,13 @@ class _DraggableItemState extends State> { EdgeDraggingAutoScroller? autoScroller; Rect? dragTarget; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + initAutoScrollerIfNeeded(context); + } + @override Widget build(BuildContext context) { initAutoScrollerIfNeeded(context); @@ -85,6 +92,8 @@ class _DraggableItemState extends State> { 'when enableAutoScroll is true.', ); } + + autoScroller?.stopAutoScroll(); autoScroller = EdgeDraggingAutoScroller( scrollable!, onScrollViewScrolled: () { From 9692c8e426956fcd9697381f40f9b214ee1f186c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 31 Jul 2023 18:21:10 +0800 Subject: [PATCH 10/10] chore: enable sidebar test --- frontend/appflowy_flutter/integration_test/runner.dart | 4 ++++ .../integration_test/sidebar/sidebar_test_runner.dart | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100644 frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index 934e213842f1d..6feeb87705cb9 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -14,6 +14,7 @@ import 'document/document_test_runner.dart' as document_test_runner; import 'import_files_test.dart' as import_files_test; import 'share_markdown_test.dart' as share_markdown_test; import 'switch_folder_test.dart' as switch_folder_test; +import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner; /// The main task runner for all integration tests in AppFlowy. /// @@ -31,6 +32,9 @@ void main() { // Document integration tests document_test_runner.startTesting(); + // Sidebar integration tests + sidebar_test_runner.startTesting(); + // Database integration tests database_cell_test.main(); database_field_test.main(); diff --git a/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart new file mode 100644 index 0000000000000..6e7e13337b686 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart @@ -0,0 +1,10 @@ +import 'package:integration_test/integration_test.dart'; + +import 'sidebar_test.dart' as sidebar_test; + +void startTesting() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Sidebar integration tests + sidebar_test.main(); +}