Parcourir la source

feat: implement draggable folder (#3083)

Lucas.Xu il y a 1 an
Parent
commit
266209caeb
55 fichiers modifiés avec 2170 ajouts et 249 suppressions
  1. 10 9
      frontend/appflowy_flutter/integration_test/database_calendar_test.dart
  2. 13 17
      frontend/appflowy_flutter/integration_test/database_cell_test.dart
  3. 11 17
      frontend/appflowy_flutter/integration_test/database_field_test.dart
  4. 1 1
      frontend/appflowy_flutter/integration_test/database_filter_test.dart
  5. 11 20
      frontend/appflowy_flutter/integration_test/database_row_page_test.dart
  6. 6 8
      frontend/appflowy_flutter/integration_test/database_row_test.dart
  7. 3 4
      frontend/appflowy_flutter/integration_test/database_setting_test.dart
  8. 3 6
      frontend/appflowy_flutter/integration_test/database_view_test.dart
  9. 13 11
      frontend/appflowy_flutter/integration_test/document/document_create_and_delete_test.dart
  10. 4 4
      frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart
  11. 4 4
      frontend/appflowy_flutter/integration_test/document/document_with_inline_math_equation_test.dart
  12. 14 8
      frontend/appflowy_flutter/integration_test/document/document_with_inline_page_test.dart
  13. 1 1
      frontend/appflowy_flutter/integration_test/document/document_with_link_test.dart
  14. 4 4
      frontend/appflowy_flutter/integration_test/document/document_with_outline_block_test.dart
  15. 5 5
      frontend/appflowy_flutter/integration_test/document/document_with_toggle_list_test.dart
  16. 10 4
      frontend/appflowy_flutter/integration_test/document/edit_document_test.dart
  17. 3 3
      frontend/appflowy_flutter/integration_test/import_files_test.dart
  18. 4 0
      frontend/appflowy_flutter/integration_test/runner.dart
  19. 9 5
      frontend/appflowy_flutter/integration_test/share_markdown_test.dart
  20. 142 0
      frontend/appflowy_flutter/integration_test/sidebar/sidebar_test.dart
  21. 10 0
      frontend/appflowy_flutter/integration_test/sidebar/sidebar_test_runner.dart
  22. 8 2
      frontend/appflowy_flutter/integration_test/tabs_test.dart
  23. 0 1
      frontend/appflowy_flutter/integration_test/util/base.dart
  24. 89 19
      frontend/appflowy_flutter/integration_test/util/common_operations.dart
  25. 4 3
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  26. 54 11
      frontend/appflowy_flutter/integration_test/util/expectation.dart
  27. 6 0
      frontend/appflowy_flutter/lib/core/config/kv_keys.dart
  28. 6 0
      frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart
  29. 2 1
      frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart
  30. 124 3
      frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart
  31. 13 1
      frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart
  32. 19 0
      frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart
  33. 4 5
      frontend/appflowy_flutter/lib/workspace/presentation/home/home_screen.dart
  34. 3 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_panel.dart
  35. 1 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/header/import/import_type.dart
  36. 1 2
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/app/section/section.dart
  37. 114 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart
  38. 94 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart
  39. 23 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart
  40. 56 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart
  41. 85 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart
  42. 63 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart
  43. 149 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart
  44. 172 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart
  45. 54 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart
  46. 130 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart
  47. 322 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart
  48. 67 0
      frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart
  49. 107 0
      frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart
  50. 25 0
      frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart
  51. 28 0
      frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart
  52. 10 2
      frontend/resources/translations/en.json
  53. 46 46
      frontend/rust-lib/Cargo.lock
  54. 9 14
      frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs
  55. 1 8
      frontend/rust-lib/flowy-folder2/src/user_default.rs

+ 10 - 9
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();

+ 13 - 17
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);
@@ -288,9 +285,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);
@@ -366,8 +363,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);

+ 11 - 17
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');

+ 1 - 1
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();
 

+ 11 - 20
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();

+ 6 - 8
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();

+ 3 - 4
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();

+ 3 - 6
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);

+ 13 - 11
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);
     });
   });
 }

+ 4 - 4
frontend/appflowy_flutter/integration_test/document/document_with_database_test.dart

@@ -73,13 +73,13 @@ Future<void> 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);

+ 4 - 4
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

+ 14 - 8
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<String> 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);

+ 1 - 1
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

+ 4 - 4
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);
 

+ 5 - 5
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

+ 10 - 4
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.

+ 3 - 3
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'];

+ 4 - 0
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();

+ 9 - 5
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 =

+ 142 - 0
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<SingleInnerViewItem>(tester.findPageName(names[3]))
+          .view
+          .parentViewId;
+      final toId = tester
+          .widget<SingleInnerViewItem>(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<SingleInnerViewItem>(tester.findPageName(gettingStated))
+          .view
+          .childViews;
+      expect(
+        childViews[0].id,
+        tester
+            .widget<SingleInnerViewItem>(tester.findPageName(names[2]))
+            .view
+            .id,
+      );
+      expect(
+        childViews[1].id,
+        tester
+            .widget<SingleInnerViewItem>(tester.findPageName(names[0]))
+            .view
+            .id,
+      );
+      expect(
+        childViews[2].id,
+        tester
+            .widget<SingleInnerViewItem>(tester.findPageName(names[3]))
+            .view
+            .id,
+      );
+    });
+  });
+}

+ 10 - 0
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();
+}

+ 8 - 2
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);

+ 0 - 1
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<void> tapButtonWithName(

+ 89 - 19
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<void> tapAddButton() async {
-    final addButton = find.byType(AddButton);
-    await tapButton(addButton);
+  Future<void> 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<void> tapNewPageButton() async {
+    final newPageButton = find.byType(SidebarNewPageButton);
+    await tapButton(newPageButton);
   }
 
   /// Tap the create document button.
   ///
-  /// Must call [tapAddButton] first.
+  /// Must call [tapAddViewButton] first.
   Future<void> tapCreateDocumentButton() async {
     await tapButtonWithName(LocaleKeys.document_menuName.tr());
   }
 
   /// Tap the create grid button.
   ///
-  /// Must call [tapAddButton] first.
+  /// Must call [tapAddViewButton] first.
   Future<void> tapCreateGridButton() async {
     await tapButtonWithName(LocaleKeys.grid_menuName.tr());
   }
 
   /// Tap the create grid button.
   ///
-  /// Must call [tapAddButton] first.
+  /// Must call [tapAddViewButton] first.
   Future<void> tapCreateCalendarButton() async {
     await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
   }
 
   /// Tap the import button.
   ///
-  /// Must call [tapAddButton] first.
+  /// Must call [tapAddViewButton] first.
   Future<void> tapImportButton() async {
     await tapButtonWithName(LocaleKeys.moreAction_import.tr());
   }
@@ -116,6 +133,7 @@ extension CommonOperations on WidgetTester {
     Finder finder, {
     Offset? offset,
     Future<void> 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<void> hoverOnPageName(
     String name, {
+    ViewLayoutPB layout = ViewLayoutPB.Document,
     Future<void> 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<void> openPage(String name) async {
-    final finder = findPageName(name);
+  Future<void> 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<void> tapPageOptionButton() async {
-    final optionButton = find.byType(ViewDisclosureButton);
+    final optionButton = find.byType(ViewMoreActionButton);
     await tapButton(optionButton);
   }
 
   /// Tap the delete page button.
   Future<void> tapDeletePageButton() async {
     await tapPageOptionButton();
-    await tapButtonWithName(ViewDisclosureAction.delete.name);
+    await tapButtonWithName(ViewMoreActionType.delete.name);
   }
 
   /// Tap the rename page button.
   Future<void> 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<void> createNewPageWithName(
-    ViewLayoutPB layout, [
+  Future<void> 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<void> simulateKeyEvent(
@@ -289,6 +331,34 @@ extension CommonOperations on WidgetTester {
     await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr()));
     await pumpAndSettle();
   }
+
+  Future<void> 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 {

+ 4 - 3
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -76,9 +76,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'];
@@ -102,7 +102,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
       paths: paths,
     );
     await tapDatabaseRawDataButton();
-    await openPage('v020');
+    await pumpAndSettle();
+    await openPage('v020', layout: ViewLayoutPB.Grid);
   }
 
   Future<void> hoverOnFirstRowOfGrid() async {

+ 54 - 11
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),
     );
   }
 }

+ 6 - 0
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';
 }

+ 6 - 0
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<MentionPageBlock> {
       return;
     }
     getIt<MenuSharedState>().latestOpenView = view;
+    getIt<TabsBloc>().add(
+      TabsEvent.openPlugin(
+        plugin: view.plugin(),
+      ),
+    );
   }
 
   Future<ViewPB?> fetchView(String pageId) async {

+ 2 - 1
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<MenuEvent, MenuState> {
             desc: event.desc ?? "",
           );
           result.fold(
-            (app) => {},
+            (app) => emit(state.copyWith(plugin: app.plugin())),
             (error) {
               Log.error(error);
               emit(state.copyWith(successOrFailure: right(error)));

+ 124 - 3
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<ViewEvent, ViewState> {
         super(ViewState.init(view)) {
     on<ViewEvent>((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<ViewEvent, ViewState> {
             ),
           );
         },
+        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<ViewEvent, ViewState> {
     await listener.stop();
     return super.close();
   }
+
+  Future<void> _loadViewsWhenExpanded(
+    Emitter<ViewState> 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<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async {
+    final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
+    final map = result.fold(
+      (l) => {},
+      (r) => jsonDecode(r),
+    );
+    if (isExpanded) {
+      map[view.id] = true;
+    } else {
+      map.remove(view.id);
+    }
+    await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));
+  }
+
+  Future<bool> _getViewIsExpanded(ViewPB view) {
+    return getIt<KeyValueStorage>().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<ViewPB, FlowyError> result) =
       ViewDidUpdate;
 }
@@ -97,12 +214,16 @@ class ViewEvent with _$ViewEvent {
 class ViewState with _$ViewState {
   const factory ViewState({
     required ViewPB view,
+    required List<ViewPB> childViews,
     required bool isEditing,
+    required bool isExpanded,
     required Either<Unit, FlowyError> successOrFailure,
   }) = _ViewState;
 
   factory ViewState.init(ViewPB view) => ViewState(
         view: view,
+        childViews: view.childViews,
+        isExpanded: false,
         isEditing: false,
         successOrFailure: left(unit),
       );

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

@@ -40,11 +40,23 @@ 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() {
+    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,
+    );
+  }
+
   PluginType get pluginType {
     switch (layout) {
       case ViewLayoutPB.Board:

+ 19 - 0
frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart

@@ -141,6 +141,7 @@ class ViewBackendService {
     return FolderEventUpdateView(payload).send();
   }
 
+  // deprecated
   static Future<Either<Unit, FlowyError>> moveView({
     required String viewId,
     required int fromIndex,
@@ -154,6 +155,24 @@ class ViewBackendService {
     return FolderEventMoveView(payload).send();
   }
 
+  /// 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<Either<Unit, FlowyError>> moveViewV2({
+    required String viewId,
+    required String newParentId,
+    required String? prevViewId,
+  }) {
+    final payload = MoveNestedViewPayloadPB(
+      viewId: viewId,
+      newParentId: newParentId,
+      prevViewId: prevViewId,
+    );
+
+    return FolderEventMoveNestedView(payload).send();
+  }
+
   Future<List<(ViewPB, List<ViewPB>)>> fetchViewsWithLayoutType(
     ViewLayoutPB? layoutType,
   ) async {

+ 4 - 5
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;
@@ -118,7 +118,7 @@ class _HomeScreenState extends State<HomeScreen> {
             buildContext: context,
           ),
         );
-        final menu = _buildHomeMenu(
+        final menu = _buildHomeSidebar(
           layout: layout,
           context: context,
         );
@@ -140,16 +140,15 @@ class _HomeScreenState extends State<HomeScreen> {
     );
   }
 
-  Widget _buildHomeMenu({
+  Widget _buildHomeSidebar({
     required HomeLayout layout,
     required BuildContext context,
   }) {
     final workspaceSetting = widget.workspaceSetting;
-    final homeMenu = HomeMenu(
+    final homeMenu = HomeSideBar(
       user: widget.user,
       workspaceSetting: workspaceSetting,
     );
-
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
   }
 

+ 3 - 0
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);
   }
 }
 

+ 1 - 0
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,
         );
       };
 

+ 1 - 2
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);

+ 114 - 0
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<ViewPB> views;
+
+  @override
+  State<PersonalFolder> createState() => _PersonalFolderState();
+}
+
+class _PersonalFolderState extends State<PersonalFolder> {
+  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<MenuSharedState>().latestOpenView = view;
+                context.read<MenuBloc>().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<PersonalFolderHeader> createState() => _PersonalFolderHeaderState();
+}
+
+class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
+  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<MenuBloc>().add(
+                      MenuEvent.createApp(
+                        LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+                      ),
+                    );
+                widget.onAdded();
+              },
+            ),
+          ]
+        ],
+      ),
+    );
+  }
+}

+ 94 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart

@@ -0,0 +1,94 @@
+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_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'
+    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<MenuBloc, MenuState>(
+        builder: (context, state) => _buildSidebar(context, state),
+        listenWhen: (p, c) => p.plugin.id != c.plugin.id,
+        listener: (context, state) => getIt<TabsBloc>().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(20),
+            // scrollable document list
+            Expanded(
+              child: SingleChildScrollView(
+                child: SidebarFolder(
+                  views: views,
+                ),
+              ),
+            ),
+            const VSpace(10),
+            // trash
+            const SidebarTrashButton(),
+            const VSpace(10),
+            // new page button
+            const Padding(
+              padding: EdgeInsets.only(left: 6.0),
+              child: SidebarNewPageButton(),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 23 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart

@@ -0,0 +1,23 @@
+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';
+
+class SidebarFolder extends StatelessWidget {
+  const SidebarFolder({
+    super.key,
+    required this.views,
+  });
+
+  final List<ViewPB> views;
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.start,
+      children: [
+        // personal
+        PersonalFolder(views: views),
+      ],
+    );
+  }
+}

+ 56 - 0
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 SidebarNewPageButton extends StatelessWidget {
+  const SidebarNewPageButton({
+    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 _showCreatePageDialog(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<void> _showCreatePageDialog(BuildContext context) async {
+    return NavigatorTextFieldDialog(
+      title: LocaleKeys.newPageText.tr(),
+      value: '',
+      confirm: (value) {
+        if (value.isNotEmpty) {
+          context.read<MenuBloc>().add(MenuEvent.createApp(value, desc: ''));
+        }
+      },
+    ).show(context);
+  }
+}

+ 85 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart

@@ -0,0 +1,85 @@
+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<MenuBloc, MenuState>(
+      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 svgWidget(
+      name,
+      size: const Size(92, 17),
+    );
+  }
+
+  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<HomeSettingBloc>()
+            .add(const HomeSettingEvent.collapseMenu()),
+        iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
+        icon: const FlowySvg(
+          name: 'home/hide_menu',
+        ),
+      ),
+    );
+  }
+}

+ 63 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart

@@ -0,0 +1,63 @@
+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<MenuSharedState>().notifier,
+      builder: (context, value, child) {
+        return FlowyHover(
+          style: HoverStyle(
+            hoverColor: AFThemeExtension.of(context).greySelect,
+          ),
+          isSelected: () => getIt<MenuSharedState>().latestOpenView == null,
+          child: SizedBox(
+            height: 26,
+            child: InkWell(
+              onTap: () {
+                getIt<MenuSharedState>().latestOpenView = null;
+                getIt<TabsBloc>().add(
+                  TabsEvent.openPlugin(
+                    plugin: makePlugin(pluginType: PluginType.trash),
+                  ),
+                );
+              },
+              child: _buildTextButton(context),
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  Widget _buildTextButton(BuildContext context) {
+    return Row(
+      children: [
+        const HSpace(6),
+        const FlowySvg(
+          size: Size(16, 16),
+          name: 'home/trash',
+        ),
+        const HSpace(6),
+        FlowyText.medium(
+          LocaleKeys.trash_text.tr(),
+        ),
+      ],
+    );
+  }
+}

+ 149 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart

@@ -0,0 +1,149 @@
+import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
+import 'package:appflowy/startup/entry_point.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<MenuUserBloc>(
+      create: (context) => getIt<MenuUserBloc>(param1: user)
+        ..add(
+          const MenuUserEvent.initial(),
+        ),
+      child: BlocBuilder<MenuUserBloc, MenuUserState>(
+        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<MenuUserBloc>().state.userProfile.iconUrl;
+    if (iconUrl.isEmpty) {
+      iconUrl = defaultUserAvatar;
+      final String name = _userName(
+        context.read<MenuUserBloc>().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<MenuUserBloc>().state.userProfile,
+    );
+    return FlowyText.medium(
+      name,
+      overflow: TextOverflow.ellipsis,
+      color: Theme.of(context).colorScheme.tertiary,
+    );
+  }
+
+  Widget _buildSettingsButton(BuildContext context) {
+    final userProfile = context.read<MenuUserBloc>().state.userProfile;
+    return Tooltip(
+      message: LocaleKeys.settings_menu_open.tr(),
+      child: IconButton(
+        onPressed: () {
+          showDialog(
+            context: context,
+            builder: (context) {
+              return BlocProvider<DocumentAppearanceCubit>.value(
+                value: BlocProvider.of<DocumentAppearanceCubit>(context),
+                child: SettingsDialog(
+                  userProfile,
+                  didLogout: () async {
+                    Navigator.of(context).pop();
+                    await FlowyRunner.run(
+                      FlowyApp(),
+                      integrationEnv(),
+                    );
+                  },
+                  dismissDialog: () => Navigator.of(context).pop(),
+                ),
+              );
+            },
+          );
+        },
+        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;
+  }
+}

+ 172 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart

@@ -0,0 +1,172 @@
+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<DraggableViewItem> createState() => _DraggableViewItemState();
+}
+
+class _DraggableViewItemState extends State<DraggableViewItem> {
+  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<ViewPB>(
+      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<ViewBloc>().add(
+              ViewEvent.move(
+                from,
+                to.parentViewId,
+                null,
+              ),
+            );
+        break;
+      case DraggableHoverPosition.bottom:
+        context.read<ViewBloc>().add(
+              ViewEvent.move(
+                from,
+                to.parentViewId,
+                to.id,
+              ),
+            );
+        break;
+      case DraggableHoverPosition.center:
+        context.read<ViewBloc>().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;
+    }
+
+    return childViews.any((v) => v.containsView(view));
+  }
+}

+ 54 - 0
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');
+    }
+  }
+}

+ 130 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart

@@ -0,0 +1,130 @@
+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/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<int>? initialDataBytes,
+    bool openAfterCreated,
+    bool createNewView,
+  ) onSelected;
+
+  List<PopoverAction> get _actions {
+    return [
+      // document, grid, kanban, calendar
+      ...pluginBuilders().map(
+        (pluginBuilder) => ViewAddButtonActionWrapper(
+          pluginBuilder: pluginBuilder,
+        ),
+      ),
+      // import from ...
+      ...getIt<PluginSandbox>().builders.whereType<DocumentPluginBuilder>().map(
+            (pluginBuilder) => ViewImportActionWrapper(
+              pluginBuilder: pluginBuilder,
+            ),
+          ),
+    ];
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return PopoverActionList<PopoverAction>(
+      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, true);
+  }
+
+  void _showViewImportAction(
+    BuildContext context,
+    ViewImportActionWrapper action,
+  ) {
+    showImportPanel(
+      parentViewId,
+      context,
+      (type, name, initialDataBytes) {
+        onSelected(action.pluginBuilder, null, null, true, false);
+      },
+    );
+  }
+}
+
+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();
+}

+ 322 - 0
frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart

@@ -0,0 +1,322 @@
+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';
+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:flowy_infra_ui/style_widget/hover.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<ViewBloc, ViewState>(
+        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<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
+}
+
+class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
+  @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<MenuSharedState>().latestOpenView?.id == widget.view.id,
+    );
+  }
+
+  Widget _buildViewItem(bool onHover) {
+    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 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<ViewBloc>()
+          .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<ViewBloc>().add(ViewEvent.setIsEditing(value)),
+        onSelected: (
+          pluginBuilder,
+          name,
+          initialDataBytes,
+          openAfterCreated,
+          createNewView,
+        ) {
+          if (createNewView) {
+            context.read<ViewBloc>().add(
+                  ViewEvent.createView(
+                    name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+                    pluginBuilder.layoutType!,
+                    openAfterCreated: openAfterCreated,
+                  ),
+                );
+          }
+          context.read<ViewBloc>().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<ViewBloc>().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<ViewBloc>().add(ViewEvent.rename(newValue));
+                },
+              ).show(context);
+              break;
+            case ViewMoreActionType.delete:
+              context.read<ViewBloc>().add(const ViewEvent.delete());
+              break;
+            case ViewMoreActionType.duplicate:
+              context.read<ViewBloc>().add(const ViewEvent.duplicate());
+              break;
+            case ViewMoreActionType.openInNewTab:
+              context.read<TabsBloc>().add(
+                    TabsEvent.openTab(
+                      plugin: widget.view.plugin(),
+                      view: widget.view,
+                    ),
+                  );
+              break;
+            default:
+              throw UnsupportedError('$action is not supported');
+          }
+        },
+      ),
+    );
+  }
+}

+ 67 - 0
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<ViewMoreActionTypeWrapper>(
+      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;
+}

+ 107 - 0
frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart

@@ -0,0 +1,107 @@
+import 'package:flutter/material.dart';
+
+class DraggableItem<T extends Object> 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<T>? onAccept;
+  final DragTargetWillAccept<T>? onWillAccept;
+  final DragTargetMove<T>? onMove;
+  final DragTargetLeave<T>? 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<DraggableItem<T>> createState() => _DraggableItemState<T>();
+}
+
+class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
+  ScrollableState? scrollable;
+  EdgeDraggingAutoScroller? autoScroller;
+  Rect? dragTarget;
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    initAutoScrollerIfNeeded(context);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    initAutoScrollerIfNeeded(context);
+
+    return DragTarget(
+      onAccept: widget.onAccept,
+      onWillAccept: widget.onWillAccept,
+      onMove: widget.onMove,
+      onLeave: widget.onLeave,
+      builder: (_, __, ___) => Draggable<T>(
+        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) {
+          autoScroller?.stopAutoScroll();
+          dragTarget = null;
+        },
+        onDraggableCanceled: (_, __) {
+          autoScroller?.stopAutoScroll();
+          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?.stopAutoScroll();
+    autoScroller = EdgeDraggingAutoScroller(
+      scrollable!,
+      onScrollViewScrolled: () {
+        if (dragTarget != null) {
+          autoScroller!.startAutoScrollIfNecessary(dragTarget!);
+        }
+      },
+      velocityScalar: 20,
+    );
+  }
+}

+ 25 - 0
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,
+    );
+  }
+}

+ 28 - 0
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);
+  });
 }

+ 10 - 2
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"
@@ -146,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": {

+ 46 - 46
frontend/rust-lib/Cargo.lock

@@ -178,9 +178,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "aws-config"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcdcf0d683fe9c23d32cf5b53c9918ea0a500375a9fb20109802552658e576c9"
+checksum = "fc00553f5f3c06ffd4510a9d576f92143618706c45ea6ff81e84ad9be9588abd"
 dependencies = [
  "aws-credential-types",
  "aws-http",
@@ -208,9 +208,9 @@ dependencies = [
 
 [[package]]
 name = "aws-credential-types"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fcdb2f7acbc076ff5ad05e7864bdb191ca70a6fd07668dc3a1a8bcd051de5ae"
+checksum = "4cb57ac6088805821f78d282c0ba8aec809f11cbee10dda19a97b03ab040ccc2"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
@@ -222,9 +222,9 @@ dependencies = [
 
 [[package]]
 name = "aws-endpoint"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cce1c41a6cfaa726adee9ebb9a56fcd2bbfd8be49fd8a04c5e20fd968330b04"
+checksum = "9c5f6f84a4f46f95a9bb71d9300b73cd67eb868bc43ae84f66ad34752299f4ac"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
@@ -236,9 +236,9 @@ dependencies = [
 
 [[package]]
 name = "aws-http"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aadbc44e7a8f3e71c8b374e03ecd972869eb91dd2bc89ed018954a52ba84bc44"
+checksum = "a754683c322f7dc5167484266489fdebdcd04d26e53c162cad1f3f949f2c5671"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-http",
@@ -281,9 +281,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "0.28.0"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8b812340d86d4a766b2ca73f740dfd47a97c2dff0c06c8517a16d88241957e4"
+checksum = "babfd626348836a31785775e3c08a4c345a5ab4c6e06dfd9167f2bee0e6295d6"
 dependencies = [
  "aws-credential-types",
  "aws-endpoint",
@@ -306,9 +306,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "0.28.0"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "265fac131fbfc188e5c3d96652ea90ecc676a934e3174eaaee523c6cec040b3b"
+checksum = "2d0fbe3c2c342bc8dfea4bb43937405a8ec06f99140a0dcb9c7b59e54dfa93a1"
 dependencies = [
  "aws-credential-types",
  "aws-endpoint",
@@ -332,9 +332,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sig-auth"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b94acb10af0c879ecd5c7bdf51cda6679a0a4f4643ce630905a77673bfa3c61"
+checksum = "84dc92a63ede3c2cbe43529cb87ffa58763520c96c6a46ca1ced80417afba845"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
@@ -346,9 +346,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sigv4"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c"
+checksum = "392fefab9d6fcbd76d518eb3b1c040b84728ab50f58df0c3c53ada4bea9d327e"
 dependencies = [
  "aws-smithy-http",
  "form_urlencoded",
@@ -365,9 +365,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-async"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880"
+checksum = "ae23b9fe7a07d0919000116c4c5c0578303fbce6fc8d32efca1f7759d4c20faf"
 dependencies = [
  "futures-util",
  "pin-project-lite",
@@ -377,9 +377,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-client"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a86aa6e21e86c4252ad6a0e3e74da9617295d8d6e374d552be7d3059c41cedd"
+checksum = "5230d25d244a51339273b8870f0f77874cd4449fb4f8f629b21188ae10cfc0ba"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
@@ -401,9 +401,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28"
+checksum = "b60e2133beb9fe6ffe0b70deca57aaeff0a35ad24a9c6fab2fd3b4f45b99fdb5"
 dependencies = [
  "aws-smithy-types",
  "bytes",
@@ -423,9 +423,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http-tower"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9"
+checksum = "3a4d94f556c86a0dd916a5d7c39747157ea8cb909ca469703e20fee33e448b67"
 dependencies = [
  "aws-smithy-http",
  "aws-smithy-types",
@@ -439,18 +439,18 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-json"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23f9f42fbfa96d095194a632fbac19f60077748eba536eb0b9fecc28659807f8"
+checksum = "5ce3d6e6ebb00b2cce379f079ad5ec508f9bcc3a9510d9b9c1840ed1d6f8af39"
 dependencies = [
  "aws-smithy-types",
 ]
 
 [[package]]
 name = "aws-smithy-query"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98819eb0b04020a1c791903533b638534ae6c12e2aceda3e6e6fba015608d51d"
+checksum = "d58edfca32ef9bfbc1ca394599e17ea329cb52d6a07359827be74235b64b3298"
 dependencies = [
  "aws-smithy-types",
  "urlencoding",
@@ -458,9 +458,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8"
+checksum = "58db46fc1f4f26be01ebdb821751b4e2482cd43aa2b64a0348fb89762defaffa"
 dependencies = [
  "base64-simd",
  "itoa",
@@ -471,18 +471,18 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-xml"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1b9d12875731bd07e767be7baad95700c3137b56730ec9ddeedb52a5e5ca63b"
+checksum = "fb557fe4995bd9ec87fb244bbb254666a971dc902a783e9da8b7711610e9664c"
 dependencies = [
  "xmlparser",
 ]
 
 [[package]]
 name = "aws-types"
-version = "0.55.3"
+version = "0.55.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dd209616cc8d7bfb82f87811a5c655dc97537f592689b18743bddf5dc5c4829"
+checksum = "de0869598bfe46ec44ffe17e063ed33336e59df90356ca8ff0e8da6f7c1d994b"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-async",
@@ -3372,9 +3372,9 @@ dependencies = [
 
 [[package]]
 name = "postgrest"
-version = "1.6.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7"
+checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b"
 dependencies = [
  "reqwest",
 ]
@@ -4030,9 +4030,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-native-certs"
-version = "0.6.3"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
 dependencies = [
  "openssl-probe",
  "rustls-pemfile",
@@ -4151,15 +4151,15 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.18"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
 
 [[package]]
 name = "serde"
-version = "1.0.175"
+version = "1.0.178"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
+checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348"
 dependencies = [
  "serde_derive",
 ]
@@ -4177,9 +4177,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.175"
+version = "1.0.178"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
+checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5085,9 +5085,9 @@ dependencies = [
 
 [[package]]
 name = "urlencoding"
-version = "2.1.3"
+version = "2.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
 
 [[package]]
 name = "utf-8"

+ 9 - 14
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(())

+ 1 - 8
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()