瀏覽代碼

feat: disable moving page into the database (#3107)

Lucas.Xu 1 年之前
父節點
當前提交
fb9bc359b8

+ 67 - 0
frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart

@@ -2,7 +2,9 @@ import 'package:appflowy/plugins/database_view/board/presentation/board_page.dar
 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_add_button.dart';
 import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
+import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:flutter/material.dart';
@@ -138,5 +140,70 @@ void main() {
             .id,
       );
     });
+
+    testWidgets('unable to move a document into a database', (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      const document = 'document';
+      await tester.createNewPageWithName(
+        name: document,
+        openAfterCreated: false,
+      );
+      tester.expectToSeePageName(document, layout: ViewLayoutPB.Document);
+
+      const grid = 'grid';
+      await tester.createNewPageWithName(
+        name: grid,
+        layout: ViewLayoutPB.Grid,
+        openAfterCreated: false,
+      );
+      tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid);
+
+      // move the document to the grid page
+      await tester.movePageToOtherPage(
+        name: document,
+        parentName: grid,
+        layout: ViewLayoutPB.Document,
+        parentLayout: ViewLayoutPB.Grid,
+      );
+
+      // it should not be moved
+      final childViews = tester
+          .widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
+          .view
+          .childViews;
+      expect(
+        childViews[0].name,
+        document,
+      );
+      expect(
+        childViews[1].name,
+        grid,
+      );
+    });
+
+    testWidgets('unable to create a new database inside the existing one',
+        (tester) async {
+      await tester.initializeAppFlowy();
+      await tester.tapGoButton();
+
+      const grid = 'grid';
+      await tester.createNewPageWithName(
+        name: grid,
+        layout: ViewLayoutPB.Grid,
+        openAfterCreated: true,
+      );
+      tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid);
+
+      await tester.hoverOnPageName(
+        grid,
+        layout: ViewLayoutPB.Grid,
+        onHover: () async {
+          expect(find.byType(ViewAddButton), findsNothing);
+          expect(find.byType(ViewMoreActionButton), findsOneWidget);
+        },
+      );
+    });
   });
 }

+ 3 - 3
frontend/appflowy_flutter/integration_test/util/common_operations.dart

@@ -351,7 +351,7 @@ extension CommonOperations on WidgetTester {
     await hoverOnPageName(
       name,
       layout: layout,
-      useLast: false,
+      useLast: true,
       onHover: () async {
         await tapFavoritePageButton();
         await pumpAndSettle();
@@ -366,7 +366,7 @@ extension CommonOperations on WidgetTester {
     await hoverOnPageName(
       name,
       layout: layout,
-      useLast: false,
+      useLast: true,
       onHover: () async {
         await tapUnfavoritePageButton();
         await pumpAndSettle();
@@ -397,7 +397,7 @@ extension CommonOperations on WidgetTester {
         break;
       default:
     }
-    await gesture.moveTo(offset);
+    await gesture.moveTo(offset, timeStamp: const Duration(milliseconds: 400));
     await gesture.up();
     await pumpAndSettle();
   }

+ 3 - 2
frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart

@@ -42,7 +42,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
           final result = await _workspaceService.createApp(
             name: event.name,
             desc: event.desc,
-            index: 0, // default to the first index
+            index: event.index,
           );
           result.fold(
             (app) => emit(state.copyWith(plugin: app.plugin())),
@@ -111,7 +111,8 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
 class MenuEvent with _$MenuEvent {
   const factory MenuEvent.initial() = _Initial;
   const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
-  const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp;
+  const factory MenuEvent.createApp(String name, {String? desc, int? index}) =
+      _CreateApp;
   const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
   const factory MenuEvent.didReceiveApps(
     Either<List<ViewPB>, FlowyError> appsOrFail,

+ 13 - 0
frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart

@@ -125,4 +125,17 @@ extension ViewLayoutExtension on ViewLayoutPB {
         throw Exception('Unknown layout type');
     }
   }
+
+  bool get isDatabaseView {
+    switch (this) {
+      case ViewLayoutPB.Grid:
+      case ViewLayoutPB.Board:
+      case ViewLayoutPB.Calendar:
+        return true;
+      case ViewLayoutPB.Document:
+        return false;
+      default:
+        throw Exception('Unknown layout type');
+    }
+  }
 }

+ 2 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart

@@ -49,6 +49,7 @@ class PersonalFolder extends StatelessWidget {
                     isFirstChild: view.id == views.first.id,
                     view: view,
                     level: 0,
+                    leftPadding: 16,
                     onSelected: (view) {
                       getIt<TabsBloc>().add(
                         TabsEvent.openPlugin(
@@ -114,6 +115,7 @@ class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
                 context.read<MenuBloc>().add(
                       MenuEvent.createApp(
                         LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+                        index: 0,
                       ),
                     );
                 widget.onAdded();

+ 6 - 1
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart

@@ -48,7 +48,12 @@ class SidebarNewPageButton extends StatelessWidget {
       value: '',
       confirm: (value) {
         if (value.isNotEmpty) {
-          context.read<MenuBloc>().add(MenuEvent.createApp(value, desc: ''));
+          context.read<MenuBloc>().add(
+                MenuEvent.createApp(
+                  value,
+                  desc: '',
+                ),
+              );
         }
       },
     ).show(context);

+ 20 - 6
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/workspace/application/view/view_bloc.dart';
+import 'package:appflowy/workspace/application/view/view_ext.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';
@@ -70,16 +71,17 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
       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);
+        final position = _computeHoverPosition(offset, renderBox.size);
+        if (!_shouldAccept(data.data, position)) {
+          return;
+        }
         setState(() {
-          position = _computeHoverPosition(offset, renderBox.size);
           Log.debug(
             'offset: $offset, position: $position, size: ${renderBox.size}',
           );
+          this.position = position;
         });
       },
       onLeave: (_) => setState(
@@ -102,6 +104,12 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
   }
 
   void _move(ViewPB from, ViewPB to) {
+    if (position == DraggableHoverPosition.center &&
+        to.layout != ViewLayoutPB.Document) {
+      // not support moving into a database
+      return;
+    }
+
     switch (position) {
       case DraggableHoverPosition.top:
         context.read<ViewBloc>().add(
@@ -136,7 +144,7 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
   }
 
   DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) {
-    final threshold = size.height / 4.0;
+    final threshold = size.height / 3.0;
     if (widget.isFirstChild && offset.dy < -5.0) {
       return DraggableHoverPosition.top;
     }
@@ -146,7 +154,13 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
     return DraggableHoverPosition.center;
   }
 
-  bool _shouldAccept(ViewPB data) {
+  bool _shouldAccept(ViewPB data, DraggableHoverPosition position) {
+    // could not move the view to a database
+    if (widget.view.layout.isDatabaseView &&
+        position == DraggableHoverPosition.center) {
+      return false;
+    }
+
     // ignore moving the view to itself
     if (data.id == widget.view.id) {
       return false;

+ 65 - 13
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart

@@ -23,6 +23,7 @@ class ViewItem extends StatelessWidget {
   const ViewItem({
     super.key,
     required this.view,
+    this.parentView,
     required this.categoryType,
     required this.level,
     this.leftPadding = 10,
@@ -32,6 +33,7 @@ class ViewItem extends StatelessWidget {
   });
 
   final ViewPB view;
+  final ViewPB? parentView;
 
   final FolderCategoryType categoryType;
 
@@ -60,6 +62,7 @@ class ViewItem extends StatelessWidget {
         builder: (context, state) {
           return InnerViewItem(
             view: state.view,
+            parentView: parentView,
             childViews: state.childViews,
             categoryType: categoryType,
             level: level,
@@ -80,18 +83,20 @@ class InnerViewItem extends StatelessWidget {
   const InnerViewItem({
     super.key,
     required this.view,
+    required this.parentView,
     required this.childViews,
     required this.categoryType,
     this.isDraggable = true,
     this.isExpanded = true,
     required this.level,
-    this.leftPadding = 10,
+    required this.leftPadding,
     required this.showActions,
     required this.onSelected,
     this.isFirstChild = false,
   });
 
   final ViewPB view;
+  final ViewPB? parentView;
   final List<ViewPB> childViews;
   final FolderCategoryType categoryType;
 
@@ -109,10 +114,13 @@ class InnerViewItem extends StatelessWidget {
   Widget build(BuildContext context) {
     Widget child = SingleInnerViewItem(
       view: view,
+      parentView: parentView,
       level: level,
       showActions: showActions,
       onSelected: onSelected,
       isExpanded: isExpanded,
+      isDraggable: isDraggable,
+      leftPadding: leftPadding,
     );
 
     // if the view is expanded and has child views, render its child views
@@ -120,12 +128,14 @@ class InnerViewItem extends StatelessWidget {
       final children = childViews.map((childView) {
         return ViewItem(
           key: ValueKey('${categoryType.name} ${childView.id}'),
+          parentView: view,
           categoryType: categoryType,
           isFirstChild: childView.id == childViews.first.id,
           view: childView,
           level: level + 1,
           onSelected: onSelected,
           isDraggable: isDraggable,
+          leftPadding: leftPadding,
         );
       }).toList();
 
@@ -139,7 +149,7 @@ class InnerViewItem extends StatelessWidget {
     }
 
     // wrap the child with DraggableItem if isDraggable is true
-    if (isDraggable) {
+    if (isDraggable && !isReferencedDatabaseView(view, parentView)) {
       child = DraggableViewItem(
         isFirstChild: isFirstChild,
         view: view,
@@ -147,10 +157,12 @@ class InnerViewItem extends StatelessWidget {
         feedback: (context) {
           return ViewItem(
             view: view,
+            parentView: parentView,
             categoryType: categoryType,
             level: level,
             onSelected: onSelected,
             isDraggable: false,
+            leftPadding: leftPadding,
           );
         },
       );
@@ -170,19 +182,23 @@ class SingleInnerViewItem extends StatefulWidget {
   const SingleInnerViewItem({
     super.key,
     required this.view,
+    required this.parentView,
     required this.isExpanded,
     required this.level,
-    this.leftPadding = 10,
+    required this.leftPadding,
+    this.isDraggable = true,
     required this.showActions,
     required this.onSelected,
   });
 
   final ViewPB view;
+  final ViewPB? parentView;
   final bool isExpanded;
 
   final int level;
   final double leftPadding;
 
+  final bool isDraggable;
   final bool showActions;
   final void Function(ViewPB) onSelected;
 
@@ -200,16 +216,16 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
       buildWhenOnHover: () => !widget.showActions,
       builder: (_, onHover) => _buildViewItem(onHover),
       isSelected: () =>
-          widget.showActions ||
-          getIt<MenuSharedState>().latestOpenView?.id == widget.view.id,
+          widget.isDraggable &&
+          (widget.showActions ||
+              getIt<MenuSharedState>().latestOpenView?.id == widget.view.id),
     );
   }
 
   Widget _buildViewItem(bool onHover) {
     final children = [
       // expand icon
-      _buildExpandedIcon(),
-      const HSpace(7),
+      _buildLeftIcon(),
       // icon
       SizedBox.square(
         dimension: 16,
@@ -229,12 +245,15 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
     if (widget.showActions || onHover) {
       // ··· more action button
       children.add(_buildViewMoreActionButton(context));
-      // + button
-      children.add(_buildViewAddButton(context));
+      // only support add button for document layout
+      if (widget.view.layout == ViewLayoutPB.Document) {
+        // + button
+        children.add(_buildViewAddButton(context));
+      }
     }
 
-    // Don't use GestureDetector here, because it doesn't response to the tap event sometimes.
-    return InkWell(
+    return GestureDetector(
+      behavior: HitTestBehavior.translucent,
       onTap: () => widget.onSelected(widget.view),
       child: SizedBox(
         height: 26,
@@ -248,8 +267,14 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
     );
   }
 
-  // > button
-  Widget _buildExpandedIcon() {
+  // > button or · button
+  // show > if the view is expandable.
+  // show · if the view can't contain child views.
+  Widget _buildLeftIcon() {
+    if (isReferencedDatabaseView(widget.view, widget.parentView)) {
+      return const _DotIconWidget();
+    }
+
     final name =
         widget.isExpanded ? 'home/drop_down_show' : 'home/drop_down_hide';
     return GestureDetector(
@@ -343,3 +368,30 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
     );
   }
 }
+
+class _DotIconWidget extends StatelessWidget {
+  const _DotIconWidget();
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(6.0),
+      child: Container(
+        width: 4,
+        height: 4,
+        decoration: BoxDecoration(
+          color: Theme.of(context).iconTheme.color,
+          borderRadius: BorderRadius.circular(2),
+        ),
+      ),
+    );
+  }
+}
+
+// workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field.
+bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
+  if (parentView == null) {
+    return false;
+  }
+  return view.layout.isDatabaseView && parentView.layout.isDatabaseView;
+}

+ 3 - 3
frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart

@@ -32,8 +32,8 @@ void main() {
     menuBloc.add(const MenuEvent.createApp("App 3"));
     await blocResponseFuture();
 
-    assert(menuBloc.state.views[0].name == 'App 3');
-    assert(menuBloc.state.views[1].name == 'App 2');
-    assert(menuBloc.state.views[2].name == 'App 1');
+    assert(menuBloc.state.views[1].name == 'App 1');
+    assert(menuBloc.state.views[2].name == 'App 2');
+    assert(menuBloc.state.views[3].name == 'App 3');
   });
 }