Przeglądaj źródła

Merge pull request #464 from AppFlowy-IO/feat_view_order

Feature: save view/app order
Nathan.fooo 3 lat temu
rodzic
commit
c3ff984c3f
35 zmienionych plików z 1510 dodań i 619 usunięć
  1. 5 3
      frontend/app_flowy/lib/startup/deps_resolver.dart
  2. 153 53
      frontend/app_flowy/lib/workspace/application/app/app_bloc.dart
  3. 31 10
      frontend/app_flowy/lib/workspace/application/app/app_service.dart
  4. 23 14
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart
  5. 367 154
      frontend/app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart
  6. 107 0
      frontend/app_flowy/lib/workspace/application/menu/menu_view_section_bloc.dart
  7. 31 10
      frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart
  8. 3 0
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  9. 27 76
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart
  10. 9 6
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  11. 39 191
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart
  12. 44 90
      frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart
  13. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/trash/menu.dart
  14. 17 0
      frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-folder/dart_event.dart
  15. 89 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pb.dart
  16. 15 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbenum.dart
  17. 24 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbjson.dart
  18. 2 0
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder/event_map.pbenum.dart
  19. 2 1
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder/event_map.pbjson.dart
  20. 5 1
      frontend/rust-lib/flowy-folder/src/event_map.rs
  21. 8 4
      frontend/rust-lib/flowy-folder/src/protobuf/model/event_map.rs
  22. 1 0
      frontend/rust-lib/flowy-folder/src/protobuf/proto/event_map.proto
  23. 13 0
      frontend/rust-lib/flowy-folder/src/services/app/controller.rs
  24. 2 0
      frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs
  25. 16 0
      frontend/rust-lib/flowy-folder/src/services/persistence/version_1/v1_impl.rs
  26. 22 0
      frontend/rust-lib/flowy-folder/src/services/persistence/version_2/v2_impl.rs
  27. 14 0
      frontend/rust-lib/flowy-folder/src/services/view/controller.rs
  28. 22 0
      frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs
  29. 1 0
      frontend/rust-lib/flowy-sdk/src/lib.rs
  30. 11 2
      frontend/rust-lib/lib-dispatch/src/byte_trait.rs
  31. 2 1
      frontend/rust-lib/lib-dispatch/src/dispatcher.rs
  32. 48 0
      shared-lib/flowy-folder-data-model/src/entities/view.rs
  33. 316 2
      shared-lib/flowy-folder-data-model/src/protobuf/model/view.rs
  34. 10 0
      shared-lib/flowy-folder-data-model/src/protobuf/proto/view.proto
  35. 30 0
      shared-lib/flowy-sync/src/client_folder/folder_pad.rs

+ 5 - 3
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -14,6 +14,7 @@ import 'package:app_flowy/workspace/application/menu/prelude.dart';
 import 'package:app_flowy/user/application/prelude.dart';
 import 'package:app_flowy/user/presentation/router.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
@@ -50,6 +51,8 @@ void _resolveUserDeps(GetIt getIt) {
 }
 
 void _resolveHomeDeps(GetIt getIt) {
+  getIt.registerSingleton(MenuSharedState());
+
   getIt.registerFactoryParam<UserListener, UserProfile, void>(
     (user, _) => UserListener(user: user),
   );
@@ -96,7 +99,6 @@ void _resolveFolderDeps(GetIt getIt) {
   getIt.registerFactoryParam<MenuBloc, UserProfile, String>(
     (user, workspaceId) => MenuBloc(
       workspaceId: workspaceId,
-      service: WorkspaceService(),
       listener: getIt<WorkspaceListener>(param1: user, param2: workspaceId),
     ),
   );
@@ -113,8 +115,8 @@ void _resolveFolderDeps(GetIt getIt) {
   getIt.registerFactoryParam<AppBloc, App, void>(
     (app, _) => AppBloc(
       app: app,
-      service: AppService(),
-      listener: AppListener(appId: app.id),
+      appService: AppService(appId: app.id),
+      appListener: AppListener(appId: app.id),
     ),
   );
 

+ 153 - 53
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -1,10 +1,14 @@
 import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/app/app_listener.dart';
 import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
+import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
@@ -13,71 +17,91 @@ part 'app_bloc.freezed.dart';
 
 class AppBloc extends Bloc<AppEvent, AppState> {
   final App app;
-  final AppService service;
-  final AppListener listener;
+  final AppService appService;
+  final AppListener appListener;
 
-  AppBloc({required this.app, required this.service, required this.listener}) : super(AppState.initial(app)) {
+  AppBloc({required this.app, required this.appService, required this.appListener}) : super(AppState.initial(app)) {
     on<AppEvent>((event, emit) async {
       await event.map(initial: (e) async {
-        listener.start(
-          viewsChanged: _handleViewsChanged,
-          appUpdated: (app) => add(AppEvent.appDidUpdate(app)),
-        );
-        await _fetchViews(emit);
+        _startListening();
+        await _loadViews(emit);
       }, createView: (CreateView value) async {
-        final viewOrFailed = await service.createView(
-          appId: app.id,
-          name: value.name,
-          desc: value.desc,
-          dataType: value.dataType,
-          pluginType: value.pluginType,
-        );
-        viewOrFailed.fold(
-          (view) => emit(state.copyWith(
-            latestCreatedView: view,
-            successOrFailure: left(unit),
-          )),
-          (error) {
-            Log.error(error);
-            emit(state.copyWith(successOrFailure: right(error)));
-          },
-        );
-      }, didReceiveViews: (e) async {
-        await handleDidReceiveViews(e.views, emit);
+        await _createView(value, emit);
+      }, didReceiveViewUpdated: (e) async {
+        await _didReceiveViewUpdated(e.views, emit);
       }, delete: (e) async {
-        final result = await service.delete(appId: app.id);
-        result.fold(
-          (unit) => emit(state.copyWith(successOrFailure: left(unit))),
-          (error) => emit(state.copyWith(successOrFailure: right(error))),
-        );
+        await _deleteView(emit);
       }, rename: (e) async {
-        final result = await service.updateApp(appId: app.id, name: e.newName);
-        result.fold(
-          (l) => emit(state.copyWith(successOrFailure: left(unit))),
-          (error) => emit(state.copyWith(successOrFailure: right(error))),
-        );
+        await _renameView(e, emit);
       }, appDidUpdate: (e) async {
         emit(state.copyWith(app: e.app));
       });
     });
   }
 
-  @override
-  Future<void> close() async {
-    await listener.close();
-    return super.close();
+  void _startListening() {
+    appListener.start(
+      viewsChanged: (result) {
+        result.fold(
+          (views) {
+            if (!isClosed) {
+              add(AppEvent.didReceiveViewUpdated(views));
+            }
+          },
+          (error) => Log.error(error),
+        );
+      },
+      appUpdated: (app) {
+        if (!isClosed) {
+          add(AppEvent.appDidUpdate(app));
+        }
+      },
+    );
   }
 
-  void _handleViewsChanged(Either<List<View>, FlowyError> result) {
+  Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
+    final result = await appService.updateApp(appId: app.id, name: e.newName);
     result.fold(
-      (views) => add(AppEvent.didReceiveViews(views)),
+      (l) => emit(state.copyWith(successOrFailure: left(unit))),
+      (error) => emit(state.copyWith(successOrFailure: right(error))),
+    );
+  }
+
+  Future<void> _deleteView(Emitter<AppState> emit) async {
+    final result = await appService.delete(appId: app.id);
+    result.fold(
+      (unit) => emit(state.copyWith(successOrFailure: left(unit))),
+      (error) => emit(state.copyWith(successOrFailure: right(error))),
+    );
+  }
+
+  Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
+    final viewOrFailed = await appService.createView(
+      appId: app.id,
+      name: value.name,
+      desc: value.desc,
+      dataType: value.dataType,
+      pluginType: value.pluginType,
+    );
+    viewOrFailed.fold(
+      (view) => emit(state.copyWith(
+        latestCreatedView: view,
+        successOrFailure: left(unit),
+      )),
       (error) {
         Log.error(error);
+        emit(state.copyWith(successOrFailure: right(error)));
       },
     );
   }
 
-  Future<void> handleDidReceiveViews(List<View> views, Emitter<AppState> emit) async {
+  @override
+  Future<void> close() async {
+    await appListener.close();
+    return super.close();
+  }
+
+  Future<void> _didReceiveViewUpdated(List<View> views, Emitter<AppState> emit) async {
     final latestCreatedView = state.latestCreatedView;
     AppState newState = state.copyWith(views: views);
     if (latestCreatedView != null) {
@@ -90,10 +114,10 @@ class AppBloc extends Bloc<AppEvent, AppState> {
     emit(newState);
   }
 
-  Future<void> _fetchViews(Emitter<AppState> emit) async {
-    final viewsOrFailed = await service.getViews(appId: app.id);
+  Future<void> _loadViews(Emitter<AppState> emit) async {
+    final viewsOrFailed = await appService.getViews(appId: app.id);
     viewsOrFailed.fold(
-      (apps) => emit(state.copyWith(views: apps)),
+      (views) => emit(state.copyWith(views: views)),
       (error) {
         Log.error(error);
         emit(state.copyWith(successOrFailure: right(error)));
@@ -113,7 +137,7 @@ class AppEvent with _$AppEvent {
   ) = CreateView;
   const factory AppEvent.delete() = Delete;
   const factory AppEvent.rename(String newName) = Rename;
-  const factory AppEvent.didReceiveViews(List<View> views) = ReceiveViews;
+  const factory AppEvent.didReceiveViewUpdated(List<View> views) = ReceiveViews;
   const factory AppEvent.appDidUpdate(App app) = AppDidUpdate;
 }
 
@@ -121,17 +145,93 @@ class AppEvent with _$AppEvent {
 class AppState with _$AppState {
   const factory AppState({
     required App app,
-    required bool isLoading,
-    required List<View>? views,
+    required List<View> views,
     View? latestCreatedView,
     required Either<Unit, FlowyError> successOrFailure,
   }) = _AppState;
 
   factory AppState.initial(App app) => AppState(
         app: app,
-        isLoading: false,
-        views: null,
-        latestCreatedView: null,
+        views: [],
         successOrFailure: left(unit),
       );
 }
+
+class AppViewDataContext extends ChangeNotifier {
+  final String appId;
+  final ValueNotifier<List<View>> _viewsNotifier = ValueNotifier([]);
+  final ValueNotifier<View?> _selectedViewNotifier = ValueNotifier(null);
+  ExpandableController expandController = ExpandableController(initialExpanded: false);
+
+  AppViewDataContext({required this.appId}) {
+    _setLatestView(getIt<MenuSharedState>().latestOpenView);
+    getIt<MenuSharedState>().addLatestViewListener((view) {
+      _setLatestView(view);
+    });
+  }
+
+  VoidCallback addSelectedViewChangeListener(void Function(View?) callback) {
+    listener() {
+      callback(_selectedViewNotifier.value);
+    }
+
+    _selectedViewNotifier.addListener(listener);
+    return listener;
+  }
+
+  void removeSelectedViewListener(VoidCallback listener) {
+    _selectedViewNotifier.removeListener(listener);
+  }
+
+  void _setLatestView(View? view) {
+    view?.freeze();
+
+    if (_selectedViewNotifier.value != view) {
+      _selectedViewNotifier.value = view;
+      _expandIfNeed();
+      notifyListeners();
+    }
+  }
+
+  View? get selectedView => _selectedViewNotifier.value;
+
+  set views(List<View> views) {
+    if (_viewsNotifier.value != views) {
+      _viewsNotifier.value = views;
+      _expandIfNeed();
+      notifyListeners();
+    }
+  }
+
+  UnmodifiableListView<View> get views => UnmodifiableListView(_viewsNotifier.value);
+
+  VoidCallback addViewsChangeListener(void Function(UnmodifiableListView<View>) callback) {
+    listener() {
+      callback(views);
+    }
+
+    _viewsNotifier.addListener(listener);
+    return listener;
+  }
+
+  void removeViewsListener(VoidCallback listener) {
+    _viewsNotifier.removeListener(listener);
+  }
+
+  void _expandIfNeed() {
+    if (_selectedViewNotifier.value == null) {
+      return;
+    }
+
+    if (!_viewsNotifier.value.contains(_selectedViewNotifier.value)) {
+      return;
+    }
+
+    if (expandController.expanded == false) {
+      // Workaround: Delay 150 milliseconds to make the smooth animation while expanding
+      Future.delayed(const Duration(milliseconds: 150), () {
+        expandController.expanded = true;
+      });
+    }
+  }
+}

+ 31 - 10
frontend/app_flowy/lib/workspace/application/app/app_service.dart

@@ -1,16 +1,23 @@
 import 'dart:async';
+
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+
 import 'package:app_flowy/plugin/plugin.dart';
 
 class AppService {
+  final String appId;
+  AppService({
+    required this.appId,
+  });
+
   Future<Either<App, FlowyError>> getAppDesc({required String appId}) {
-    final request = AppId.create()..value = appId;
+    final payload = AppId.create()..value = appId;
 
-    return FolderEventReadApp(request).send();
+    return FolderEventReadApp(payload).send();
   }
 
   Future<Either<View, FlowyError>> createView({
@@ -20,20 +27,20 @@ class AppService {
     required PluginDataType dataType,
     required PluginType pluginType,
   }) {
-    final request = CreateViewPayload.create()
+    final payload = CreateViewPayload.create()
       ..belongToId = appId
       ..name = name
       ..desc = desc
       ..dataType = dataType
       ..pluginType = pluginType;
 
-    return FolderEventCreateView(request).send();
+    return FolderEventCreateView(payload).send();
   }
 
   Future<Either<List<View>, FlowyError>> getViews({required String appId}) {
-    final request = AppId.create()..value = appId;
+    final payload = AppId.create()..value = appId;
 
-    return FolderEventReadApp(request).send().then((result) {
+    return FolderEventReadApp(payload).send().then((result) {
       return result.fold(
         (app) => left(app.belongings.items),
         (error) => right(error),
@@ -47,11 +54,25 @@ class AppService {
   }
 
   Future<Either<Unit, FlowyError>> updateApp({required String appId, String? name}) {
-    UpdateAppPayload request = UpdateAppPayload.create()..appId = appId;
+    UpdateAppPayload payload = UpdateAppPayload.create()..appId = appId;
 
     if (name != null) {
-      request.name = name;
+      payload.name = name;
     }
-    return FolderEventUpdateApp(request).send();
+    return FolderEventUpdateApp(payload).send();
+  }
+
+  Future<Either<Unit, FlowyError>> moveView({
+    required String viewId,
+    required int fromIndex,
+    required int toIndex,
+  }) {
+    final payload = MoveFolderItemPayload.create()
+      ..itemId = viewId
+      ..from = fromIndex
+      ..to = toIndex
+      ..ty = MoveFolderItemType.MoveView;
+
+    return FolderEventMoveFolderItem(payload).send();
   }
 }

+ 23 - 14
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart

@@ -12,11 +12,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 part 'menu_bloc.freezed.dart';
 
 class MenuBloc extends Bloc<MenuEvent, MenuState> {
-  final WorkspaceService service;
+  final WorkspaceService _workspaceService;
   final WorkspaceListener listener;
   final String workspaceId;
 
-  MenuBloc({required this.workspaceId, required this.service, required this.listener}) : super(MenuState.initial()) {
+  MenuBloc({required this.workspaceId, required this.listener})
+      : _workspaceService = WorkspaceService(workspaceId: workspaceId),
+        super(MenuState.initial()) {
     on<MenuEvent>((event, emit) async {
       await event.map(
         initial: (e) async {
@@ -30,15 +32,21 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
         openPage: (e) async {
           emit(state.copyWith(plugin: e.plugin));
         },
-        createApp: (CreateApp event) async {
+        createApp: (_CreateApp event) async {
           await _performActionOnCreateApp(event, emit);
         },
         didReceiveApps: (e) async {
           emit(e.appsOrFail.fold(
-            (apps) => state.copyWith(apps: some(apps), successOrFailure: left(unit)),
+            (apps) => state.copyWith(apps: apps, successOrFailure: left(unit)),
             (err) => state.copyWith(successOrFailure: right(err)),
           ));
         },
+        moveApp: (_MoveApp value) {
+          if (state.apps.length > value.fromIndex) {
+            final app = state.apps[value.fromIndex];
+            _workspaceService.moveApp(appId: app.id, fromIndex: value.fromIndex, toIndex: value.toIndex);
+          }
+        },
       );
     });
   }
@@ -49,8 +57,8 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
     return super.close();
   }
 
-  Future<void> _performActionOnCreateApp(CreateApp event, Emitter<MenuState> emit) async {
-    final result = await service.createApp(workspaceId: workspaceId, name: event.name, desc: event.desc ?? "");
+  Future<void> _performActionOnCreateApp(_CreateApp event, Emitter<MenuState> emit) async {
+    final result = await _workspaceService.createApp(name: event.name, desc: event.desc ?? "");
     result.fold(
       (app) => {},
       (error) {
@@ -62,9 +70,9 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
 
   // ignore: unused_element
   Future<void> _fetchApps(Emitter<MenuState> emit) async {
-    final appsOrFail = await service.getApps(workspaceId: workspaceId);
+    final appsOrFail = await _workspaceService.getApps();
     emit(appsOrFail.fold(
-      (apps) => state.copyWith(apps: some(apps)),
+      (apps) => state.copyWith(apps: apps),
       (error) {
         Log.error(error);
         return state.copyWith(successOrFailure: right(error));
@@ -83,24 +91,25 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
 @freezed
 class MenuEvent with _$MenuEvent {
   const factory MenuEvent.initial() = _Initial;
-  const factory MenuEvent.collapse() = Collapse;
-  const factory MenuEvent.openPage(Plugin plugin) = OpenPage;
-  const factory MenuEvent.createApp(String name, {String? desc}) = CreateApp;
-  const factory MenuEvent.didReceiveApps(Either<List<App>, FlowyError> appsOrFail) = ReceiveApps;
+  const factory MenuEvent.collapse() = _Collapse;
+  const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
+  const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp;
+  const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
+  const factory MenuEvent.didReceiveApps(Either<List<App>, FlowyError> appsOrFail) = _ReceiveApps;
 }
 
 @freezed
 class MenuState with _$MenuState {
   const factory MenuState({
     required bool isCollapse,
-    required Option<List<App>> apps,
+    required List<App> apps,
     required Either<Unit, FlowyError> successOrFailure,
     required Plugin plugin,
   }) = _MenuState;
 
   factory MenuState.initial() => MenuState(
         isCollapse: false,
-        apps: none(),
+        apps: [],
         successOrFailure: left(unit),
         plugin: makePlugin(pluginType: DefaultPlugin.blank.type()),
       );

Plik diff jest za duży
+ 367 - 154
frontend/app_flowy/lib/workspace/application/menu/menu_bloc.freezed.dart


+ 107 - 0
frontend/app_flowy/lib/workspace/application/menu/menu_view_section_bloc.dart

@@ -0,0 +1,107 @@
+import 'dart:async';
+
+import 'package:app_flowy/workspace/application/app/app_bloc.dart';
+import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+part 'menu_view_section_bloc.freezed.dart';
+
+class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
+  void Function()? _viewsListener;
+  void Function()? _selectedViewlistener;
+  final AppViewDataContext _appViewData;
+  late final AppService _appService;
+
+  ViewSectionBloc({
+    required AppViewDataContext appViewData,
+  })  : _appService = AppService(appId: appViewData.appId),
+        _appViewData = appViewData,
+        super(ViewSectionState.initial(appViewData)) {
+    on<ViewSectionEvent>((event, emit) async {
+      await event.map(
+        initial: (e) async {
+          _startListening();
+        },
+        setSelectedView: (_SetSelectedView value) {
+          _setSelectView(value, emit);
+        },
+        didReceiveViewUpdated: (_DidReceiveViewUpdated value) {
+          emit(state.copyWith(views: value.views));
+        },
+        moveView: (_MoveView value) async {
+          await _moveView(value);
+        },
+      );
+    });
+  }
+
+  void _startListening() {
+    _viewsListener = _appViewData.addViewsChangeListener((views) {
+      if (!isClosed) {
+        add(ViewSectionEvent.didReceiveViewUpdated(views));
+      }
+    });
+    _selectedViewlistener = _appViewData.addSelectedViewChangeListener((view) {
+      if (!isClosed) {
+        add(ViewSectionEvent.setSelectedView(view));
+      }
+    });
+  }
+
+  void _setSelectView(_SetSelectedView value, Emitter<ViewSectionState> emit) {
+    if (state.views.contains(value.view)) {
+      emit(state.copyWith(selectedView: value.view));
+    } else {
+      emit(state.copyWith(selectedView: null));
+    }
+  }
+
+  Future<void> _moveView(_MoveView value) async {
+    if (value.fromIndex < state.views.length) {
+      final viewId = state.views[value.fromIndex].id;
+      final result = await _appService.moveView(
+        viewId: viewId,
+        fromIndex: value.fromIndex,
+        toIndex: value.toIndex,
+      );
+      result.fold((l) => null, (err) => Log.error(err));
+    }
+  }
+
+  @override
+  Future<void> close() async {
+    if (_selectedViewlistener != null) {
+      _appViewData.removeSelectedViewListener(_selectedViewlistener!);
+    }
+
+    if (_viewsListener != null) {
+      _appViewData.removeViewsListener(_viewsListener!);
+    }
+
+    return super.close();
+  }
+}
+
+@freezed
+class ViewSectionEvent with _$ViewSectionEvent {
+  const factory ViewSectionEvent.initial() = _Initial;
+  const factory ViewSectionEvent.setSelectedView(View? view) = _SetSelectedView;
+  const factory ViewSectionEvent.moveView(int fromIndex, int toIndex) = _MoveView;
+  const factory ViewSectionEvent.didReceiveViewUpdated(List<View> views) = _DidReceiveViewUpdated;
+}
+
+@freezed
+class ViewSectionState with _$ViewSectionState {
+  const factory ViewSectionState({
+    required List<View> views,
+    View? selectedView,
+  }) = _ViewSectionState;
+
+  factory ViewSectionState.initial(AppViewDataContext appViewData) => ViewSectionState(
+        views: appViewData.views,
+        selectedView: appViewData.selectedView,
+      );
+}

+ 31 - 10
frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart

@@ -1,24 +1,31 @@
 import 'dart:async';
+
 import 'package:dartz/dartz.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart' show MoveFolderItemPayload, MoveFolderItemType;
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/workspace.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+
 import 'package:app_flowy/generated/locale_keys.g.dart';
 
 class WorkspaceService {
-  Future<Either<App, FlowyError>> createApp({required String workspaceId, required String name, required String desc}) {
-    final request = CreateAppPayload.create()
+  final String workspaceId;
+  WorkspaceService({
+    required this.workspaceId,
+  });
+  Future<Either<App, FlowyError>> createApp({required String name, required String desc}) {
+    final payload = CreateAppPayload.create()
       ..name = name
       ..workspaceId = workspaceId
       ..desc = desc;
-    return FolderEventCreateApp(request).send();
+    return FolderEventCreateApp(payload).send();
   }
 
-  Future<Either<Workspace, FlowyError>> getWorkspace({required String workspaceId}) {
-    final request = WorkspaceId.create()..value = workspaceId;
-    return FolderEventReadWorkspaces(request).send().then((result) {
+  Future<Either<Workspace, FlowyError>> getWorkspace() {
+    final payload = WorkspaceId.create()..value = workspaceId;
+    return FolderEventReadWorkspaces(payload).send().then((result) {
       return result.fold(
         (workspaces) {
           assert(workspaces.items.length == 1);
@@ -34,13 +41,27 @@ class WorkspaceService {
     });
   }
 
-  Future<Either<List<App>, FlowyError>> getApps({required String workspaceId}) {
-    final request = WorkspaceId.create()..value = workspaceId;
-    return FolderEventReadWorkspaceApps(request).send().then((result) {
+  Future<Either<List<App>, FlowyError>> getApps() {
+    final payload = WorkspaceId.create()..value = workspaceId;
+    return FolderEventReadWorkspaceApps(payload).send().then((result) {
       return result.fold(
         (apps) => left(apps.items),
         (error) => right(error),
       );
     });
   }
+
+  Future<Either<Unit, FlowyError>> moveApp({
+    required String appId,
+    required int fromIndex,
+    required int toIndex,
+  }) {
+    final payload = MoveFolderItemPayload.create()
+      ..itemId = appId
+      ..from = fromIndex
+      ..to = toIndex
+      ..ty = MoveFolderItemType.MoveApp;
+
+    return FolderEventMoveFolderItem(payload).send();
+  }
 }

+ 3 - 0
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -121,6 +121,9 @@ class _HomeScreenState extends State<HomeScreen> {
       collapsedNotifier: getIt<HomeStackManager>().collapsedNotifier,
     );
 
+    final latestView = widget.workspaceSetting.hasLatestView() ? widget.workspaceSetting.latestView : null;
+    getIt<MenuSharedState>().latestOpenView = latestView;
+
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
   }
 

+ 27 - 76
frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart

@@ -2,7 +2,6 @@ import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/startup/startup.dart';
@@ -19,11 +18,11 @@ class MenuApp extends StatefulWidget {
 }
 
 class _MenuAppState extends State<MenuApp> {
-  late AppDataNotifier notifier;
+  late AppViewDataContext viewDataContext;
 
   @override
   void initState() {
-    notifier = AppDataNotifier();
+    viewDataContext = AppViewDataContext(appId: widget.app.id);
     super.initState();
   }
 
@@ -39,32 +38,36 @@ class _MenuAppState extends State<MenuApp> {
           },
         ),
       ],
-      child: BlocSelector<AppBloc, AppState, AppDataNotifier>(
-        selector: (state) {
-          final menuSharedState = Provider.of<MenuSharedState>(context, listen: false);
-          if (state.latestCreatedView != null) {
-            menuSharedState.forcedOpenView.value = state.latestCreatedView!;
-          }
-
-          notifier.views = state.views;
-          notifier.selectedView = menuSharedState.selectedView.value;
-          return notifier;
-        },
-        builder: (context, notifier) => ChangeNotifierProvider.value(
-          value: notifier,
-          child: Consumer(
-            builder: (BuildContext context, AppDataNotifier notifier, Widget? child) {
-              return expandableWrapper(context, notifier);
-            },
+      child: MultiBlocListener(
+        listeners: [
+          BlocListener<AppBloc, AppState>(
+            listenWhen: (p, c) => p.latestCreatedView != c.latestCreatedView,
+            listener: (context, state) => getIt<MenuSharedState>().latestOpenView = state.latestCreatedView,
+          ),
+          BlocListener<AppBloc, AppState>(
+            listenWhen: (p, c) => p.views != c.views,
+            listener: (context, state) => viewDataContext.views = state.views,
           ),
+        ],
+        child: BlocBuilder<AppBloc, AppState>(
+          builder: (context, state) {
+            return ChangeNotifierProvider.value(
+              value: viewDataContext,
+              child: Consumer<AppViewDataContext>(
+                builder: (context, viewDataContext, _) {
+                  return expandableWrapper(context, viewDataContext);
+                },
+              ),
+            );
+          },
         ),
       ),
     );
   }
 
-  ExpandableNotifier expandableWrapper(BuildContext context, AppDataNotifier notifier) {
+  ExpandableNotifier expandableWrapper(BuildContext context, AppViewDataContext viewDataContext) {
     return ExpandableNotifier(
-      controller: notifier.expandController,
+      controller: viewDataContext.expandController,
       child: ScrollOnExpand(
         scrollOnExpand: false,
         scrollOnCollapse: false,
@@ -83,7 +86,7 @@ class _MenuAppState extends State<MenuApp> {
                 value: Provider.of<AppearanceSettingModel>(context, listen: true),
                 child: MenuAppHeader(widget.app),
               ),
-              expanded: _renderViewSection(notifier),
+              expanded: ViewSection(appViewData: viewDataContext),
               collapsed: const SizedBox(),
             ),
           ],
@@ -92,20 +95,9 @@ class _MenuAppState extends State<MenuApp> {
     );
   }
 
-  Widget _renderViewSection(AppDataNotifier notifier) {
-    return MultiProvider(
-      providers: [ChangeNotifierProvider.value(value: notifier)],
-      child: Consumer(
-        builder: (context, AppDataNotifier notifier, child) {
-          return ViewSection(appData: notifier);
-        },
-      ),
-    );
-  }
-
   @override
   void dispose() {
-    notifier.dispose();
+    viewDataContext.dispose();
     super.dispose();
   }
 }
@@ -119,44 +111,3 @@ class MenuAppSizes {
   static double scale = 1;
   static double get expandedPadding => iconSize * scale + headerPadding;
 }
-
-class AppDataNotifier extends ChangeNotifier {
-  List<View> _views = [];
-  View? _selectedView;
-  ExpandableController expandController = ExpandableController(initialExpanded: false);
-
-  AppDataNotifier();
-
-  set selectedView(View? view) {
-    _selectedView = view;
-
-    if (view != null && _views.isNotEmpty) {
-      final isExpanded = _views.contains(view);
-      if (expandController.expanded == false && expandController.expanded != isExpanded) {
-        // Workaround: Delay 150 milliseconds to make the smooth animation while expanding
-        Future.delayed(const Duration(milliseconds: 150), () {
-          expandController.expanded = isExpanded;
-        });
-      }
-    }
-  }
-
-  View? get selectedView => _selectedView;
-
-  set views(List<View>? views) {
-    if (views == null) {
-      if (_views.isNotEmpty) {
-        _views = List.empty(growable: false);
-        notifyListeners();
-      }
-      return;
-    }
-
-    if (_views != views) {
-      _views = views;
-      notifyListeners();
-    }
-  }
-
-  List<View> get views => _views;
-}

+ 9 - 6
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -40,12 +40,15 @@ class ViewSectionItem extends StatelessWidget {
       ],
       child: BlocBuilder<ViewBloc, ViewState>(
         builder: (context, state) {
-          return InkWell(
-            onTap: () => onSelected(context.read<ViewBloc>().state.view),
-            child: FlowyHover(
-              style: HoverStyle(hoverColor: theme.bg3),
-              builder: (_, onHover) => _render(context, onHover, state, theme.iconColor),
-              setSelected: () => state.isEditing || isSelected,
+          return Padding(
+            padding: const EdgeInsets.symmetric(vertical: 4),
+            child: InkWell(
+              onTap: () => onSelected(context.read<ViewBloc>().state.view),
+              child: FlowyHover(
+                style: HoverStyle(hoverColor: theme.bg3),
+                builder: (_, onHover) => _render(context, onHover, state, theme.iconColor),
+                setSelected: () => state.isEditing || isSelected,
+              ),
             ),
           );
         },

+ 39 - 191
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart

@@ -1,221 +1,69 @@
-import 'dart:async';
-import 'dart:developer';
-
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/app/app_bloc.dart';
+import 'package:app_flowy/workspace/application/menu/menu_view_section_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_ext.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:reorderables/reorderables.dart';
-import 'package:styled_widget/styled_widget.dart';
+
 import 'item.dart';
 
 class ViewSection extends StatelessWidget {
-  final AppDataNotifier appData;
-  const ViewSection({Key? key, required this.appData}) : super(key: key);
+  final AppViewDataContext appViewData;
+  const ViewSection({Key? key, required this.appViewData}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    // The ViewSectionNotifier will be updated after AppDataNotifier changed passed by parent widget
-    return ChangeNotifierProxyProvider<AppDataNotifier, ViewSectionNotifier>(
-      create: (_) {
-        return ViewSectionNotifier(
-          context: context,
-          views: appData.views,
-          initialSelectedView: appData.selectedView,
-        );
+    return BlocProvider(
+      create: (context) {
+        final bloc = ViewSectionBloc(appViewData: appViewData);
+        bloc.add(const ViewSectionEvent.initial());
+        return bloc;
       },
-      update: (_, notifier, controller) => controller!..update(notifier),
-      child: Consumer(builder: (context, ViewSectionNotifier notifier, child) {
-        return RenderSectionItems(views: notifier.views);
-      }),
+      child: BlocListener<ViewSectionBloc, ViewSectionState>(
+        listenWhen: (p, c) => p.selectedView != c.selectedView,
+        listener: (context, state) {
+          if (state.selectedView != null) {
+            WidgetsBinding.instance?.addPostFrameCallback((_) {
+              getIt<HomeStackManager>().setPlugin(state.selectedView!.plugin());
+            });
+          }
+        },
+        child: BlocBuilder<ViewSectionBloc, ViewSectionState>(
+          builder: (context, state) {
+            return _reorderableColum(context, state);
+          },
+        ),
+      ),
     );
   }
 
-  // Widget _renderSectionItems(BuildContext context, List<View> views) {
-  //   List<Widget> viewWidgets = [];
-  //   if (views.isNotEmpty) {
-  //     viewWidgets = views
-  //         .map(
-  //           (view) => ViewSectionItem(
-  //             view: view,
-  //             isSelected: _isViewSelected(context, view.id),
-  //             onSelected: (view) {
-  //               context.read<ViewSectionNotifier>().selectedView = view;
-  //               Provider.of<MenuSharedState>(context, listen: false).selectedView.value = view;
-  //             },
-  //           ).padding(vertical: 4),
-  //         )
-  //         .toList(growable: false);
-  //   }
-
-  //   return Column(children: viewWidgets);
-  // }
-
-  // bool _isViewSelected(BuildContext context, String viewId) {
-  //   final view = context.read<ViewSectionNotifier>().selectedView;
-  //   if (view == null) {
-  //     return false;
-  //   }
-  //   return view.id == viewId;
-  // }
-}
-
-class RenderSectionItems extends StatefulWidget {
-  const RenderSectionItems({Key? key, required this.views}) : super(key: key);
-
-  final List<View> views;
-
-  @override
-  State<RenderSectionItems> createState() => _RenderSectionItemsState();
-}
-
-class _RenderSectionItemsState extends State<RenderSectionItems> {
-  List<View> views = <View>[];
-
-  /// Maps the hasmap value of the section items to their index in the reorderable list.
-  //TODO @gaganyadav80: Retain this map to persist the order of the items.
-  final Map<String, int> _sectionItemIndex = <String, int>{};
-
-  void _initItemList() {
-    views.addAll(widget.views);
-
-    for (int i = 0; i < views.length; i++) {
-      if (_sectionItemIndex[views[i].id] == null) {
-        _sectionItemIndex[views[i].id] = i;
-      }
-    }
-  }
-
-  @override
-  void initState() {
-    super.initState();
-    _initItemList();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    if (views.isEmpty) {
-      _initItemList();
-    }
+  ReorderableColumn _reorderableColum(BuildContext context, ViewSectionState state) {
+    final children = state.views.map((view) {
+      return ViewSectionItem(
+        key: ValueKey(view.id),
+        view: view,
+        isSelected: _isViewSelected(state, view.id),
+        onSelected: (view) => getIt<MenuSharedState>().latestOpenView = view,
+      );
+    }).toList();
 
-    log("BUILD: Section items: ${views.length}");
     return ReorderableColumn(
       needsLongPressDraggable: false,
       onReorder: (oldIndex, index) {
-        setState(() {
-          // int index = newIndex > oldIndex ? newIndex - 1 : newIndex;
-          View section = views.removeAt(oldIndex);
-          views.insert(index, section);
-
-          _sectionItemIndex[section.id] = index;
-        });
+        context.read<ViewSectionBloc>().add(ViewSectionEvent.moveView(oldIndex, index));
       },
-      children: List.generate(
-        views.length,
-        (index) {
-          return Container(
-            key: ValueKey(views[index].id),
-            child: views
-                .map(
-                  (view) => ViewSectionItem(
-                    view: view,
-                    isSelected: _isViewSelected(context, view.id),
-                    onSelected: (view) {
-                      context.read<ViewSectionNotifier>().selectedView = view;
-                      Provider.of<MenuSharedState>(context, listen: false).selectedView.value = view;
-                    },
-                  ).padding(vertical: 4),
-                )
-                .toList()[index],
-          );
-        },
-      ),
+      children: children,
     );
   }
 
-  bool _isViewSelected(BuildContext context, String viewId) {
-    final view = context.read<ViewSectionNotifier>().selectedView;
+  bool _isViewSelected(ViewSectionState state, String viewId) {
+    final view = state.selectedView;
     if (view == null) {
       return false;
     }
     return view.id == viewId;
   }
 }
-
-class ViewSectionNotifier with ChangeNotifier {
-  bool isDisposed = false;
-  List<View> _views;
-  View? _selectedView;
-  Timer? _notifyListenerOperation;
-
-  ViewSectionNotifier({
-    required BuildContext context,
-    required List<View> views,
-    View? initialSelectedView,
-  })  : _views = views,
-        _selectedView = initialSelectedView {
-    final menuSharedState = Provider.of<MenuSharedState>(context, listen: false);
-    // The forcedOpenView will be the view after creating the new view
-    menuSharedState.forcedOpenView.addPublishListener((forcedOpenView) {
-      selectedView = forcedOpenView;
-    });
-
-    menuSharedState.selectedView.addListener(() {
-      // Cancel the selected view of this section by setting the selectedView to null
-      // that will notify the listener to refresh the ViewSection UI
-      if (menuSharedState.selectedView.value != _selectedView) {
-        selectedView = null;
-      }
-    });
-  }
-
-  set views(List<View> views) {
-    if (_views != views) {
-      _views = views;
-      _notifyListeners();
-    }
-  }
-
-  List<View> get views => _views;
-
-  set selectedView(View? view) {
-    if (_selectedView == view) {
-      return;
-    }
-    _selectedView = view;
-    _notifyListeners();
-
-    if (view != null) {
-      WidgetsBinding.instance?.addPostFrameCallback((_) {
-        getIt<HomeStackManager>().setPlugin(view.plugin());
-      });
-    } else {
-      // do nothing
-    }
-  }
-
-  View? get selectedView => _selectedView;
-
-  void update(AppDataNotifier notifier) {
-    views = notifier.views;
-  }
-
-  void _notifyListeners() {
-    _notifyListenerOperation?.cancel();
-    _notifyListenerOperation = Timer(const Duration(milliseconds: 30), () {
-      if (!isDisposed) {
-        notifyListeners();
-      }
-    });
-  }
-
-  @override
-  void dispose() {
-    isDisposed = true;
-    _notifyListenerOperation?.cancel();
-    super.dispose();
-  }
-}

+ 44 - 90
frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart

@@ -13,7 +13,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/workspace.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_infra/time/duration.dart';
@@ -27,7 +26,7 @@ import 'app/menu_app.dart';
 import 'app/create_button.dart';
 import 'menu_user.dart';
 
-class HomeMenu extends StatefulWidget {
+class HomeMenu extends StatelessWidget {
   final PublishNotifier<bool> _collapsedNotifier;
   final UserProfile user;
   final CurrentWorkspaceSetting workspaceSetting;
@@ -40,22 +39,13 @@ class HomeMenu extends StatefulWidget {
   })  : _collapsedNotifier = collapsedNotifier,
         super(key: key);
 
-  @override
-  State<HomeMenu> createState() => _HomeMenuState();
-}
-
-class _HomeMenuState extends State<HomeMenu> {
-  /// Maps the hashmap of the menu items to their index in reorderable list view.
-  //TODO @gaganyadav80: Retain this map to persist on app restarts.
-  final Map<int, int> _menuItemIndex = <int, int>{};
-
   @override
   Widget build(BuildContext context) {
     return MultiBlocProvider(
       providers: [
         BlocProvider<MenuBloc>(
           create: (context) {
-            final menuBloc = getIt<MenuBloc>(param1: widget.user, param2: widget.workspaceSetting.workspace.id);
+            final menuBloc = getIt<MenuBloc>(param1: user, param2: workspaceSetting.workspace.id);
             menuBloc.add(const MenuEvent.initial());
             return menuBloc;
           },
@@ -72,7 +62,7 @@ class _HomeMenuState extends State<HomeMenu> {
           BlocListener<MenuBloc, MenuState>(
             listenWhen: (p, c) => p.isCollapse != c.isCollapse,
             listener: (context, state) {
-              widget._collapsedNotifier.value = state.isCollapse;
+              _collapsedNotifier.value = state.isCollapse;
             },
           )
         ],
@@ -88,30 +78,24 @@ class _HomeMenuState extends State<HomeMenu> {
     final theme = context.watch<AppTheme>();
     return Container(
       color: theme.bg1,
-      child: ChangeNotifierProvider(
-        create: (_) =>
-            MenuSharedState(view: widget.workspaceSetting.hasLatestView() ? widget.workspaceSetting.latestView : null),
-        child: Consumer(builder: (context, MenuSharedState sharedState, child) {
-          return Column(
-            mainAxisAlignment: MainAxisAlignment.start,
-            children: [
-              Expanded(
-                child: Column(
-                  mainAxisAlignment: MainAxisAlignment.start,
-                  children: [
-                    const MenuTopBar(),
-                    const VSpace(10),
-                    _renderApps(context),
-                  ],
-                ).padding(horizontal: Insets.l),
-              ),
-              const VSpace(20),
-              _renderTrash(context).padding(horizontal: Insets.l),
-              const VSpace(20),
-              _renderNewAppButton(context),
-            ],
-          );
-        }),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        children: [
+          Expanded(
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              children: [
+                const MenuTopBar(),
+                const VSpace(10),
+                _renderApps(context),
+              ],
+            ).padding(horizontal: Insets.l),
+          ),
+          const VSpace(20),
+          const MenuTrash().padding(horizontal: Insets.l),
+          const VSpace(20),
+          _renderNewAppButton(context),
+        ],
       ),
     );
   }
@@ -123,56 +107,18 @@ class _HomeMenuState extends State<HomeMenu> {
         child: ScrollConfiguration(
           behavior: const ScrollBehavior().copyWith(scrollbars: false),
           child: BlocSelector<MenuBloc, MenuState, List<Widget>>(
-            selector: (state) {
-              List<Widget> menuItems = [];
-              // menuItems.add(MenuUser(user));
-              List<MenuApp> appWidgets =
-                  state.apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList());
-              // menuItems.addAll(appWidgets);
-              for (int i = 0; i < appWidgets.length; i++) {
-                if (_menuItemIndex[appWidgets[i].key.hashCode] == null) {
-                  _menuItemIndex[appWidgets[i].key.hashCode] = i;
-                }
-
-                menuItems.insert(_menuItemIndex[appWidgets[i].key.hashCode]!, appWidgets[i]);
-              }
-
-              return menuItems;
-            },
+            selector: (state) => state.apps.map((app) => MenuApp(app)).toList(),
             builder: (context, menuItems) {
               return ReorderableListView.builder(
                 itemCount: menuItems.length,
                 buildDefaultDragHandles: false,
                 header: Padding(
                   padding: EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding),
-                  child: MenuUser(widget.user),
+                  child: MenuUser(user),
                 ),
-                onReorder: (oldIndex, newIndex) {
-                  int index = newIndex > oldIndex ? newIndex - 1 : newIndex;
-
-                  Widget menu = menuItems.removeAt(oldIndex);
-                  menuItems.insert(index, menu);
-
-                  final menuBloc = context.read<MenuBloc>();
-                  menuBloc.state.apps.forEach((a) {
-                    var app = a.removeAt(oldIndex);
-                    a.insert(index, app);
-                  });
-
-                  _menuItemIndex[menu.key.hashCode] = index;
-                },
+                onReorder: (oldIndex, newIndex) => context.read<MenuBloc>().add(MenuEvent.moveApp(oldIndex, newIndex)),
                 physics: StyledScrollPhysics(),
                 itemBuilder: (BuildContext context, int index) {
-                  //? @gaganyadav80: To mimic the ListView.separated behavior, we need to add a padding.
-                  // EdgeInsets padding = EdgeInsets.zero;
-                  // if (index == 0) {
-                  //   padding = EdgeInsets.only(bottom: MenuAppSizes.appVPadding / 2);
-                  // } else if (index == menuItems.length - 1) {
-                  //   padding = EdgeInsets.only(top: MenuAppSizes.appVPadding / 2);
-                  // } else {
-                  //   padding = EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2);
-                  // }
-
                   return ReorderableDragStartListener(
                     key: ValueKey(menuItems[index].hashCode),
                     index: index,
@@ -190,10 +136,6 @@ class _HomeMenuState extends State<HomeMenu> {
     );
   }
 
-  Widget _renderTrash(BuildContext context) {
-    return const MenuTrash();
-  }
-
   Widget _renderNewAppButton(BuildContext context) {
     return NewAppButton(
       press: (appName) => context.read<MenuBloc>().add(MenuEvent.createApp(appName, desc: "")),
@@ -201,18 +143,30 @@ class _HomeMenuState extends State<HomeMenu> {
   }
 }
 
-class MenuSharedState extends ChangeNotifier {
-  PublishNotifier<View> forcedOpenView = PublishNotifier();
-  ValueNotifier<View?> selectedView = ValueNotifier<View?>(null);
+class MenuSharedState {
+  final ValueNotifier<View?> _latestOpenView = ValueNotifier<View?>(null);
 
   MenuSharedState({View? view}) {
-    if (view != null) {
-      selectedView.value = view;
+    _latestOpenView.value = view;
+  }
+
+  View? get latestOpenView => _latestOpenView.value;
+
+  set latestOpenView(View? view) {
+    _latestOpenView.value = view;
+  }
+
+  VoidCallback addLatestViewListener(void Function(View?) callback) {
+    listener() {
+      callback(_latestOpenView.value);
     }
 
-    forcedOpenView.addPublishListener((view) {
-      selectedView.value = view;
-    });
+    _latestOpenView.addListener(listener);
+    return listener;
+  }
+
+  void removeLatestViewListener(VoidCallback listener) {
+    _latestOpenView.removeListener(listener);
   }
 }
 

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/trash/menu.dart

@@ -21,7 +21,7 @@ class MenuTrash extends StatelessWidget {
       height: 26,
       child: InkWell(
         onTap: () {
-          Provider.of<MenuSharedState>(context, listen: false).selectedView.value = null;
+          getIt<MenuSharedState>().latestOpenView = null;
           getIt<HomeStackManager>().setPlugin(makePlugin(pluginType: DefaultPlugin.trash.type()));
         },
         child: _render(context),

+ 17 - 0
frontend/app_flowy/packages/flowy_sdk/lib/dispatch/dart_event/flowy-folder/dart_event.dart

@@ -301,6 +301,23 @@ class FolderEventCloseView {
     }
 }
 
+class FolderEventMoveFolderItem {
+     MoveFolderItemPayload request;
+     FolderEventMoveFolderItem(this.request);
+
+    Future<Either<Unit, FlowyError>> send() {
+    final request = FFIRequest.create()
+          ..event = FolderEvent.MoveFolderItem.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (bytes) => left(unit),
+           (errBytes) => right(FlowyError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class FolderEventReadTrash {
     FolderEventReadTrash();
 

+ 89 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pb.dart

@@ -891,3 +891,92 @@ class UpdateViewParams extends $pb.GeneratedMessage {
   void clearThumbnail() => clearField(4);
 }
 
+class MoveFolderItemPayload extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'MoveFolderItemPayload', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'itemId')
+    ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'from', $pb.PbFieldType.O3)
+    ..a<$core.int>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'to', $pb.PbFieldType.O3)
+    ..e<MoveFolderItemType>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ty', $pb.PbFieldType.OE, defaultOrMaker: MoveFolderItemType.MoveApp, valueOf: MoveFolderItemType.valueOf, enumValues: MoveFolderItemType.values)
+    ..hasRequiredFields = false
+  ;
+
+  MoveFolderItemPayload._() : super();
+  factory MoveFolderItemPayload({
+    $core.String? itemId,
+    $core.int? from,
+    $core.int? to,
+    MoveFolderItemType? ty,
+  }) {
+    final _result = create();
+    if (itemId != null) {
+      _result.itemId = itemId;
+    }
+    if (from != null) {
+      _result.from = from;
+    }
+    if (to != null) {
+      _result.to = to;
+    }
+    if (ty != null) {
+      _result.ty = ty;
+    }
+    return _result;
+  }
+  factory MoveFolderItemPayload.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory MoveFolderItemPayload.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  MoveFolderItemPayload clone() => MoveFolderItemPayload()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  MoveFolderItemPayload copyWith(void Function(MoveFolderItemPayload) updates) => super.copyWith((message) => updates(message as MoveFolderItemPayload)) as MoveFolderItemPayload; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static MoveFolderItemPayload create() => MoveFolderItemPayload._();
+  MoveFolderItemPayload createEmptyInstance() => create();
+  static $pb.PbList<MoveFolderItemPayload> createRepeated() => $pb.PbList<MoveFolderItemPayload>();
+  @$core.pragma('dart2js:noInline')
+  static MoveFolderItemPayload getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<MoveFolderItemPayload>(create);
+  static MoveFolderItemPayload? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get itemId => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set itemId($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasItemId() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearItemId() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.int get from => $_getIZ(1);
+  @$pb.TagNumber(2)
+  set from($core.int v) { $_setSignedInt32(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasFrom() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearFrom() => clearField(2);
+
+  @$pb.TagNumber(3)
+  $core.int get to => $_getIZ(2);
+  @$pb.TagNumber(3)
+  set to($core.int v) { $_setSignedInt32(2, v); }
+  @$pb.TagNumber(3)
+  $core.bool hasTo() => $_has(2);
+  @$pb.TagNumber(3)
+  void clearTo() => clearField(3);
+
+  @$pb.TagNumber(4)
+  MoveFolderItemType get ty => $_getN(3);
+  @$pb.TagNumber(4)
+  set ty(MoveFolderItemType v) { setField(4, v); }
+  @$pb.TagNumber(4)
+  $core.bool hasTy() => $_has(3);
+  @$pb.TagNumber(4)
+  void clearTy() => clearField(4);
+}
+

+ 15 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbenum.dart

@@ -24,3 +24,18 @@ class ViewDataType extends $pb.ProtobufEnum {
   const ViewDataType._($core.int v, $core.String n) : super(v, n);
 }
 
+class MoveFolderItemType extends $pb.ProtobufEnum {
+  static const MoveFolderItemType MoveApp = MoveFolderItemType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MoveApp');
+  static const MoveFolderItemType MoveView = MoveFolderItemType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MoveView');
+
+  static const $core.List<MoveFolderItemType> values = <MoveFolderItemType> [
+    MoveApp,
+    MoveView,
+  ];
+
+  static final $core.Map<$core.int, MoveFolderItemType> _byValue = $pb.ProtobufEnum.initByValue(values);
+  static MoveFolderItemType? valueOf($core.int value) => _byValue[value];
+
+  const MoveFolderItemType._($core.int v, $core.String n) : super(v, n);
+}
+

+ 24 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder-data-model/view.pbjson.dart

@@ -19,6 +19,17 @@ const ViewDataType$json = const {
 
 /// Descriptor for `ViewDataType`. Decode as a `google.protobuf.EnumDescriptorProto`.
 final $typed_data.Uint8List viewDataTypeDescriptor = $convert.base64Decode('CgxWaWV3RGF0YVR5cGUSDQoJVGV4dEJsb2NrEAASCAoER3JpZBAB');
+@$core.Deprecated('Use moveFolderItemTypeDescriptor instead')
+const MoveFolderItemType$json = const {
+  '1': 'MoveFolderItemType',
+  '2': const [
+    const {'1': 'MoveApp', '2': 0},
+    const {'1': 'MoveView', '2': 1},
+  ],
+};
+
+/// Descriptor for `MoveFolderItemType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List moveFolderItemTypeDescriptor = $convert.base64Decode('ChJNb3ZlRm9sZGVySXRlbVR5cGUSCwoHTW92ZUFwcBAAEgwKCE1vdmVWaWV3EAE=');
 @$core.Deprecated('Use viewDescriptor instead')
 const View$json = const {
   '1': 'View',
@@ -142,3 +153,16 @@ const UpdateViewParams$json = const {
 
 /// Descriptor for `UpdateViewParams`. Decode as a `google.protobuf.DescriptorProto`.
 final $typed_data.Uint8List updateViewParamsDescriptor = $convert.base64Decode('ChBVcGRhdGVWaWV3UGFyYW1zEhcKB3ZpZXdfaWQYASABKAlSBnZpZXdJZBIUCgRuYW1lGAIgASgJSABSBG5hbWUSFAoEZGVzYxgDIAEoCUgBUgRkZXNjEh4KCXRodW1ibmFpbBgEIAEoCUgCUgl0aHVtYm5haWxCDQoLb25lX29mX25hbWVCDQoLb25lX29mX2Rlc2NCEgoQb25lX29mX3RodW1ibmFpbA==');
+@$core.Deprecated('Use moveFolderItemPayloadDescriptor instead')
+const MoveFolderItemPayload$json = const {
+  '1': 'MoveFolderItemPayload',
+  '2': const [
+    const {'1': 'item_id', '3': 1, '4': 1, '5': 9, '10': 'itemId'},
+    const {'1': 'from', '3': 2, '4': 1, '5': 5, '10': 'from'},
+    const {'1': 'to', '3': 3, '4': 1, '5': 5, '10': 'to'},
+    const {'1': 'ty', '3': 4, '4': 1, '5': 14, '6': '.MoveFolderItemType', '10': 'ty'},
+  ],
+};
+
+/// Descriptor for `MoveFolderItemPayload`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List moveFolderItemPayloadDescriptor = $convert.base64Decode('ChVNb3ZlRm9sZGVySXRlbVBheWxvYWQSFwoHaXRlbV9pZBgBIAEoCVIGaXRlbUlkEhIKBGZyb20YAiABKAVSBGZyb20SDgoCdG8YAyABKAVSAnRvEiMKAnR5GAQgASgOMhMuTW92ZUZvbGRlckl0ZW1UeXBlUgJ0eQ==');

+ 2 - 0
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder/event_map.pbenum.dart

@@ -28,6 +28,7 @@ class FolderEvent extends $pb.ProtobufEnum {
   static const FolderEvent CopyLink = FolderEvent._(206, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CopyLink');
   static const FolderEvent SetLatestView = FolderEvent._(207, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'SetLatestView');
   static const FolderEvent CloseView = FolderEvent._(208, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CloseView');
+  static const FolderEvent MoveFolderItem = FolderEvent._(209, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MoveFolderItem');
   static const FolderEvent ReadTrash = FolderEvent._(300, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ReadTrash');
   static const FolderEvent PutbackTrash = FolderEvent._(301, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PutbackTrash');
   static const FolderEvent DeleteTrash = FolderEvent._(302, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteTrash');
@@ -53,6 +54,7 @@ class FolderEvent extends $pb.ProtobufEnum {
     CopyLink,
     SetLatestView,
     CloseView,
+    MoveFolderItem,
     ReadTrash,
     PutbackTrash,
     DeleteTrash,

+ 2 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-folder/event_map.pbjson.dart

@@ -30,6 +30,7 @@ const FolderEvent$json = const {
     const {'1': 'CopyLink', '2': 206},
     const {'1': 'SetLatestView', '2': 207},
     const {'1': 'CloseView', '2': 208},
+    const {'1': 'MoveFolderItem', '2': 209},
     const {'1': 'ReadTrash', '2': 300},
     const {'1': 'PutbackTrash', '2': 301},
     const {'1': 'DeleteTrash', '2': 302},
@@ -39,4 +40,4 @@ const FolderEvent$json = const {
 };
 
 /// Descriptor for `FolderEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List folderEventDescriptor = $convert.base64Decode('CgtGb2xkZXJFdmVudBITCg9DcmVhdGVXb3Jrc3BhY2UQABIUChBSZWFkQ3VyV29ya3NwYWNlEAESEgoOUmVhZFdvcmtzcGFjZXMQAhITCg9EZWxldGVXb3Jrc3BhY2UQAxIRCg1PcGVuV29ya3NwYWNlEAQSFQoRUmVhZFdvcmtzcGFjZUFwcHMQBRINCglDcmVhdGVBcHAQZRINCglEZWxldGVBcHAQZhILCgdSZWFkQXBwEGcSDQoJVXBkYXRlQXBwEGgSDwoKQ3JlYXRlVmlldxDJARINCghSZWFkVmlldxDKARIPCgpVcGRhdGVWaWV3EMsBEg8KCkRlbGV0ZVZpZXcQzAESEgoNRHVwbGljYXRlVmlldxDNARINCghDb3B5TGluaxDOARISCg1TZXRMYXRlc3RWaWV3EM8BEg4KCUNsb3NlVmlldxDQARIOCglSZWFkVHJhc2gQrAISEQoMUHV0YmFja1RyYXNoEK0CEhAKC0RlbGV0ZVRyYXNoEK4CEhQKD1Jlc3RvcmVBbGxUcmFzaBCvAhITCg5EZWxldGVBbGxUcmFzaBCwAg==');
+final $typed_data.Uint8List folderEventDescriptor = $convert.base64Decode('CgtGb2xkZXJFdmVudBITCg9DcmVhdGVXb3Jrc3BhY2UQABIUChBSZWFkQ3VyV29ya3NwYWNlEAESEgoOUmVhZFdvcmtzcGFjZXMQAhITCg9EZWxldGVXb3Jrc3BhY2UQAxIRCg1PcGVuV29ya3NwYWNlEAQSFQoRUmVhZFdvcmtzcGFjZUFwcHMQBRINCglDcmVhdGVBcHAQZRINCglEZWxldGVBcHAQZhILCgdSZWFkQXBwEGcSDQoJVXBkYXRlQXBwEGgSDwoKQ3JlYXRlVmlldxDJARINCghSZWFkVmlldxDKARIPCgpVcGRhdGVWaWV3EMsBEg8KCkRlbGV0ZVZpZXcQzAESEgoNRHVwbGljYXRlVmlldxDNARINCghDb3B5TGluaxDOARISCg1TZXRMYXRlc3RWaWV3EM8BEg4KCUNsb3NlVmlldxDQARITCg5Nb3ZlRm9sZGVySXRlbRDRARIOCglSZWFkVHJhc2gQrAISEQoMUHV0YmFja1RyYXNoEK0CEhAKC0RlbGV0ZVRyYXNoEK4CEhQKD1Jlc3RvcmVBbGxUcmFzaBCvAhITCg5EZWxldGVBbGxUcmFzaBCwAg==');

+ 5 - 1
frontend/rust-lib/flowy-folder/src/event_map.rs

@@ -62,7 +62,8 @@ pub fn create(folder: Arc<FolderManager>) -> Module {
         .event(FolderEvent::DeleteView, delete_view_handler)
         .event(FolderEvent::DuplicateView, duplicate_view_handler)
         .event(FolderEvent::SetLatestView, set_latest_view_handler)
-        .event(FolderEvent::CloseView, close_view_handler);
+        .event(FolderEvent::CloseView, close_view_handler)
+        .event(FolderEvent::MoveFolderItem, move_item_handler);
 
     module = module
         .event(FolderEvent::ReadTrash, read_trash_handler)
@@ -131,6 +132,9 @@ pub enum FolderEvent {
     #[event(input = "ViewId")]
     CloseView = 208,
 
+    #[event(input = "MoveFolderItemPayload")]
+    MoveFolderItem = 209,
+
     #[event(output = "RepeatedTrash")]
     ReadTrash = 300,
 

+ 8 - 4
frontend/rust-lib/flowy-folder/src/protobuf/model/event_map.rs

@@ -43,6 +43,7 @@ pub enum FolderEvent {
     CopyLink = 206,
     SetLatestView = 207,
     CloseView = 208,
+    MoveFolderItem = 209,
     ReadTrash = 300,
     PutbackTrash = 301,
     DeleteTrash = 302,
@@ -75,6 +76,7 @@ impl ::protobuf::ProtobufEnum for FolderEvent {
             206 => ::std::option::Option::Some(FolderEvent::CopyLink),
             207 => ::std::option::Option::Some(FolderEvent::SetLatestView),
             208 => ::std::option::Option::Some(FolderEvent::CloseView),
+            209 => ::std::option::Option::Some(FolderEvent::MoveFolderItem),
             300 => ::std::option::Option::Some(FolderEvent::ReadTrash),
             301 => ::std::option::Option::Some(FolderEvent::PutbackTrash),
             302 => ::std::option::Option::Some(FolderEvent::DeleteTrash),
@@ -104,6 +106,7 @@ impl ::protobuf::ProtobufEnum for FolderEvent {
             FolderEvent::CopyLink,
             FolderEvent::SetLatestView,
             FolderEvent::CloseView,
+            FolderEvent::MoveFolderItem,
             FolderEvent::ReadTrash,
             FolderEvent::PutbackTrash,
             FolderEvent::DeleteTrash,
@@ -137,7 +140,7 @@ impl ::protobuf::reflect::ProtobufValue for FolderEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fevent_map.proto*\xae\x03\n\x0bFolderEvent\x12\x13\n\x0fCreateWorks\
+    \n\x0fevent_map.proto*\xc3\x03\n\x0bFolderEvent\x12\x13\n\x0fCreateWorks\
     pace\x10\0\x12\x14\n\x10ReadCurWorkspace\x10\x01\x12\x12\n\x0eReadWorksp\
     aces\x10\x02\x12\x13\n\x0fDeleteWorkspace\x10\x03\x12\x11\n\rOpenWorkspa\
     ce\x10\x04\x12\x15\n\x11ReadWorkspaceApps\x10\x05\x12\r\n\tCreateApp\x10\
@@ -146,9 +149,10 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x12\x0f\n\nUpdateView\x10\xcb\x01\x12\x0f\n\nDeleteView\x10\xcc\x01\x12\
     \x12\n\rDuplicateView\x10\xcd\x01\x12\r\n\x08CopyLink\x10\xce\x01\x12\
     \x12\n\rSetLatestView\x10\xcf\x01\x12\x0e\n\tCloseView\x10\xd0\x01\x12\
-    \x0e\n\tReadTrash\x10\xac\x02\x12\x11\n\x0cPutbackTrash\x10\xad\x02\x12\
-    \x10\n\x0bDeleteTrash\x10\xae\x02\x12\x14\n\x0fRestoreAllTrash\x10\xaf\
-    \x02\x12\x13\n\x0eDeleteAllTrash\x10\xb0\x02b\x06proto3\
+    \x13\n\x0eMoveFolderItem\x10\xd1\x01\x12\x0e\n\tReadTrash\x10\xac\x02\
+    \x12\x11\n\x0cPutbackTrash\x10\xad\x02\x12\x10\n\x0bDeleteTrash\x10\xae\
+    \x02\x12\x14\n\x0fRestoreAllTrash\x10\xaf\x02\x12\x13\n\x0eDeleteAllTras\
+    h\x10\xb0\x02b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 0
frontend/rust-lib/flowy-folder/src/protobuf/proto/event_map.proto

@@ -19,6 +19,7 @@ enum FolderEvent {
     CopyLink = 206;
     SetLatestView = 207;
     CloseView = 208;
+    MoveFolderItem = 209;
     ReadTrash = 300;
     PutbackTrash = 301;
     DeleteTrash = 302;

+ 13 - 0
frontend/rust-lib/flowy-folder/src/services/app/controller.rs

@@ -95,6 +95,19 @@ impl AppController {
         Ok(())
     }
 
+    pub(crate) async fn move_app(&self, app_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        let _ = self
+            .persistence
+            .begin_transaction(|transaction| {
+                let _ = transaction.move_app(app_id, from, to)?;
+                let app = transaction.read_app(app_id)?;
+                let _ = notify_apps_changed(&app.workspace_id, self.trash_controller.clone(), &transaction)?;
+                Ok(())
+            })
+            .await?;
+        Ok(())
+    }
+
     pub(crate) async fn read_local_apps(&self, ids: Vec<String>) -> Result<Vec<App>, FlowyError> {
         let apps = self
             .persistence

+ 2 - 0
frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs

@@ -34,12 +34,14 @@ pub trait FolderPersistenceTransaction {
     fn read_app(&self, app_id: &str) -> FlowyResult<App>;
     fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>>;
     fn delete_app(&self, app_id: &str) -> FlowyResult<App>;
+    fn move_app(&self, app_id: &str, from: usize, to: usize) -> FlowyResult<()>;
 
     fn create_view(&self, view: View) -> FlowyResult<()>;
     fn read_view(&self, view_id: &str) -> FlowyResult<View>;
     fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>>;
     fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()>;
     fn delete_view(&self, view_id: &str) -> FlowyResult<()>;
+    fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()>;
 
     fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()>;
     fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash>;

+ 16 - 0
frontend/rust-lib/flowy-folder/src/services/persistence/version_1/v1_impl.rs

@@ -63,6 +63,10 @@ impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
         Ok(App::from(table))
     }
 
+    fn move_app(&self, _app_id: &str, _from: usize, _to: usize) -> FlowyResult<()> {
+        Ok(())
+    }
+
     fn create_view(&self, view: View) -> FlowyResult<()> {
         let _ = ViewTableSql::create_view(view, &*self.0)?;
         Ok(())
@@ -89,6 +93,10 @@ impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
         Ok(())
     }
 
+    fn move_view(&self, _view_id: &str, _from: usize, _to: usize) -> FlowyResult<()> {
+        Ok(())
+    }
+
     fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
         let _ = TrashTableSql::create_trash(trashes, &*self.0)?;
         Ok(())
@@ -160,6 +168,10 @@ where
         (**self).delete_app(app_id)
     }
 
+    fn move_app(&self, _app_id: &str, _from: usize, _to: usize) -> FlowyResult<()> {
+        Ok(())
+    }
+
     fn create_view(&self, view: View) -> FlowyResult<()> {
         (**self).create_view(view)
     }
@@ -180,6 +192,10 @@ where
         (**self).delete_view(view_id)
     }
 
+    fn move_view(&self, _view_id: &str, _from: usize, _to: usize) -> FlowyResult<()> {
+        Ok(())
+    }
+
     fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
         (**self).create_trash(trashes)
     }

+ 22 - 0
frontend/rust-lib/flowy-folder/src/services/persistence/version_2/v2_impl.rs

@@ -83,6 +83,13 @@ impl FolderPersistenceTransaction for ClientFolderEditor {
         Ok(app)
     }
 
+    fn move_app(&self, app_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        if let Some(change) = self.folder.write().move_app(app_id, from, to)? {
+            let _ = self.apply_change(change)?;
+        }
+        Ok(())
+    }
+
     fn create_view(&self, view: View) -> FlowyResult<()> {
         if let Some(change) = self.folder.write().create_view(view)? {
             let _ = self.apply_change(change)?;
@@ -118,6 +125,13 @@ impl FolderPersistenceTransaction for ClientFolderEditor {
         Ok(())
     }
 
+    fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        if let Some(change) = self.folder.write().move_view(view_id, from, to)? {
+            let _ = self.apply_change(change)?;
+        }
+        Ok(())
+    }
+
     fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
         if let Some(change) = self.folder.write().create_trash(trashes)? {
             let _ = self.apply_change(change)?;
@@ -178,6 +192,10 @@ where
         (**self).delete_app(app_id)
     }
 
+    fn move_app(&self, app_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        (**self).move_app(app_id, from, to)
+    }
+
     fn create_view(&self, view: View) -> FlowyResult<()> {
         (**self).create_view(view)
     }
@@ -198,6 +216,10 @@ where
         (**self).delete_view(view_id)
     }
 
+    fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()> {
+        (**self).move_view(view_id, from, to)
+    }
+
     fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
         (**self).create_trash(trashes)
     }

+ 14 - 0
frontend/rust-lib/flowy-folder/src/services/view/controller.rs

@@ -154,6 +154,20 @@ impl ViewController {
         Ok(())
     }
 
+    #[tracing::instrument(level = "debug", skip(self), err)]
+    pub(crate) async fn move_view(&self, view_id: &str, from: usize, to: usize) -> Result<(), FlowyError> {
+        let _ = self
+            .persistence
+            .begin_transaction(|transaction| {
+                let _ = transaction.move_view(view_id, from, to)?;
+                let view = transaction.read_view(view_id)?;
+                let _ = notify_views_changed(&view.belong_to_id, self.trash_controller.clone(), &transaction)?;
+                Ok(())
+            })
+            .await?;
+        Ok(())
+    }
+
     #[tracing::instrument(level = "debug", skip(self), err)]
     pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> {
         let view = self

+ 22 - 0
frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs

@@ -1,3 +1,4 @@
+use crate::services::AppController;
 use crate::{
     entities::{
         trash::Trash,
@@ -8,6 +9,7 @@ use crate::{
     errors::FlowyError,
     services::{TrashController, ViewController},
 };
+use flowy_folder_data_model::entities::view::{MoveFolderItemParams, MoveFolderItemPayload, MoveFolderItemType};
 use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
 use std::{convert::TryInto, sync::Arc};
 
@@ -83,6 +85,26 @@ pub(crate) async fn close_view_handler(
     Ok(())
 }
 
+#[tracing::instrument(level = "debug", skip_all, err)]
+pub(crate) async fn move_item_handler(
+    data: Data<MoveFolderItemPayload>,
+    view_controller: AppData<Arc<ViewController>>,
+    app_controller: AppData<Arc<AppController>>,
+) -> Result<(), FlowyError> {
+    let params: MoveFolderItemParams = data.into_inner().try_into()?;
+    match params.ty {
+        MoveFolderItemType::MoveApp => {
+            let _ = app_controller.move_app(&params.item_id, params.from, params.to).await?;
+        }
+        MoveFolderItemType::MoveView => {
+            let _ = view_controller
+                .move_view(&params.item_id, params.from, params.to)
+                .await?;
+        }
+    }
+    Ok(())
+}
+
 #[tracing::instrument(level = "debug", skip(data, controller), err)]
 pub(crate) async fn duplicate_view_handler(
     data: Data<ViewId>,

+ 1 - 0
frontend/rust-lib/flowy-sdk/src/lib.rs

@@ -75,6 +75,7 @@ fn crate_log_filter(level: String) -> String {
     filters.push(format!("lib_ws={}", level));
     filters.push(format!("lib_infra={}", level));
     filters.push(format!("flowy_sync={}", level));
+    // filters.push(format!("lib_dispatch={}", level));
 
     filters.push(format!("dart_ffi={}", "info"));
     filters.push(format!("flowy_database={}", "info"));

+ 11 - 2
frontend/rust-lib/lib-dispatch/src/byte_trait.rs

@@ -47,8 +47,17 @@ where
     T: std::convert::TryFrom<Bytes, Error = protobuf::ProtobufError>,
 {
     fn parse_from_bytes(bytes: Bytes) -> Result<Self, DispatchError> {
-        let data = T::try_from(bytes)?;
-        Ok(data)
+        match T::try_from(bytes) {
+            Ok(data) => Ok(data),
+            Err(e) => {
+                tracing::error!(
+                    "Parse payload to {} failed with error: {:?}",
+                    std::any::type_name::<T>(),
+                    e
+                );
+                Err(e.into())
+            }
+        }
     }
 }
 

+ 2 - 1
frontend/rust-lib/lib-dispatch/src/dispatcher.rs

@@ -139,13 +139,14 @@ impl Service<DispatchContext> for DispatchService {
                 // print_module_map_info(&module_map);
                 match module_map.get(&request.event) {
                     Some(module) => {
+                        tracing::trace!("Handle event: {:?} by {:?}", &request.event, module.name);
                         let fut = module.new_service(());
                         let service_fut = fut.await?.call(request);
                         service_fut.await
                     }
                     None => {
                         let msg = format!("Can not find the event handler. {:?}", request);
-                        log::error!("{}", msg);
+                        tracing::error!("{}", msg);
                         Err(InternalError::HandleNotFound(msg).into())
                     }
                 }

+ 48 - 0
shared-lib/flowy-folder-data-model/src/entities/view.rs

@@ -286,6 +286,54 @@ impl TryInto<UpdateViewParams> for UpdateViewPayload {
     }
 }
 
+#[derive(ProtoBuf_Enum)]
+pub enum MoveFolderItemType {
+    MoveApp = 0,
+    MoveView = 1,
+}
+
+impl std::default::Default for MoveFolderItemType {
+    fn default() -> Self {
+        MoveFolderItemType::MoveApp
+    }
+}
+
+#[derive(Default, ProtoBuf)]
+pub struct MoveFolderItemPayload {
+    #[pb(index = 1)]
+    pub item_id: String,
+
+    #[pb(index = 2)]
+    pub from: i32,
+
+    #[pb(index = 3)]
+    pub to: i32,
+
+    #[pb(index = 4)]
+    pub ty: MoveFolderItemType,
+}
+
+pub struct MoveFolderItemParams {
+    pub item_id: String,
+    pub from: usize,
+    pub to: usize,
+    pub ty: MoveFolderItemType,
+}
+
+impl TryInto<MoveFolderItemParams> for MoveFolderItemPayload {
+    type Error = ErrorCode;
+
+    fn try_into(self) -> Result<MoveFolderItemParams, Self::Error> {
+        let view_id = ViewIdentify::parse(self.item_id)?.0;
+        Ok(MoveFolderItemParams {
+            item_id: view_id,
+            from: self.from as usize,
+            to: self.to as usize,
+            ty: self.ty,
+        })
+    }
+}
+
 // impl<'de> Deserialize<'de> for ViewDataType {
 //     fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
 //     where

+ 316 - 2
shared-lib/flowy-folder-data-model/src/protobuf/model/view.rs

@@ -2777,6 +2777,266 @@ impl ::protobuf::reflect::ProtobufValue for UpdateViewParams {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct MoveFolderItemPayload {
+    // message fields
+    pub item_id: ::std::string::String,
+    pub from: i32,
+    pub to: i32,
+    pub ty: MoveFolderItemType,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a MoveFolderItemPayload {
+    fn default() -> &'a MoveFolderItemPayload {
+        <MoveFolderItemPayload as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl MoveFolderItemPayload {
+    pub fn new() -> MoveFolderItemPayload {
+        ::std::default::Default::default()
+    }
+
+    // string item_id = 1;
+
+
+    pub fn get_item_id(&self) -> &str {
+        &self.item_id
+    }
+    pub fn clear_item_id(&mut self) {
+        self.item_id.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_item_id(&mut self, v: ::std::string::String) {
+        self.item_id = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_item_id(&mut self) -> &mut ::std::string::String {
+        &mut self.item_id
+    }
+
+    // Take field
+    pub fn take_item_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.item_id, ::std::string::String::new())
+    }
+
+    // int32 from = 2;
+
+
+    pub fn get_from(&self) -> i32 {
+        self.from
+    }
+    pub fn clear_from(&mut self) {
+        self.from = 0;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_from(&mut self, v: i32) {
+        self.from = v;
+    }
+
+    // int32 to = 3;
+
+
+    pub fn get_to(&self) -> i32 {
+        self.to
+    }
+    pub fn clear_to(&mut self) {
+        self.to = 0;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_to(&mut self, v: i32) {
+        self.to = v;
+    }
+
+    // .MoveFolderItemType ty = 4;
+
+
+    pub fn get_ty(&self) -> MoveFolderItemType {
+        self.ty
+    }
+    pub fn clear_ty(&mut self) {
+        self.ty = MoveFolderItemType::MoveApp;
+    }
+
+    // Param is passed by value, moved
+    pub fn set_ty(&mut self, v: MoveFolderItemType) {
+        self.ty = v;
+    }
+}
+
+impl ::protobuf::Message for MoveFolderItemPayload {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.item_id)?;
+                },
+                2 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    let tmp = is.read_int32()?;
+                    self.from = tmp;
+                },
+                3 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeVarint {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    let tmp = is.read_int32()?;
+                    self.to = tmp;
+                },
+                4 => {
+                    ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.ty, 4, &mut self.unknown_fields)?
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.item_id.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.item_id);
+        }
+        if self.from != 0 {
+            my_size += ::protobuf::rt::value_size(2, self.from, ::protobuf::wire_format::WireTypeVarint);
+        }
+        if self.to != 0 {
+            my_size += ::protobuf::rt::value_size(3, self.to, ::protobuf::wire_format::WireTypeVarint);
+        }
+        if self.ty != MoveFolderItemType::MoveApp {
+            my_size += ::protobuf::rt::enum_size(4, self.ty);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.item_id.is_empty() {
+            os.write_string(1, &self.item_id)?;
+        }
+        if self.from != 0 {
+            os.write_int32(2, self.from)?;
+        }
+        if self.to != 0 {
+            os.write_int32(3, self.to)?;
+        }
+        if self.ty != MoveFolderItemType::MoveApp {
+            os.write_enum(4, ::protobuf::ProtobufEnum::value(&self.ty))?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> MoveFolderItemPayload {
+        MoveFolderItemPayload::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "item_id",
+                |m: &MoveFolderItemPayload| { &m.item_id },
+                |m: &mut MoveFolderItemPayload| { &mut m.item_id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
+                "from",
+                |m: &MoveFolderItemPayload| { &m.from },
+                |m: &mut MoveFolderItemPayload| { &mut m.from },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
+                "to",
+                |m: &MoveFolderItemPayload| { &m.to },
+                |m: &mut MoveFolderItemPayload| { &mut m.to },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<MoveFolderItemType>>(
+                "ty",
+                |m: &MoveFolderItemPayload| { &m.ty },
+                |m: &mut MoveFolderItemPayload| { &mut m.ty },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<MoveFolderItemPayload>(
+                "MoveFolderItemPayload",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static MoveFolderItemPayload {
+        static instance: ::protobuf::rt::LazyV2<MoveFolderItemPayload> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(MoveFolderItemPayload::new)
+    }
+}
+
+impl ::protobuf::Clear for MoveFolderItemPayload {
+    fn clear(&mut self) {
+        self.item_id.clear();
+        self.from = 0;
+        self.to = 0;
+        self.ty = MoveFolderItemType::MoveApp;
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for MoveFolderItemPayload {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for MoveFolderItemPayload {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 #[derive(Clone,PartialEq,Eq,Debug,Hash)]
 pub enum ViewDataType {
     TextBlock = 0,
@@ -2827,6 +3087,56 @@ impl ::protobuf::reflect::ProtobufValue for ViewDataType {
     }
 }
 
+#[derive(Clone,PartialEq,Eq,Debug,Hash)]
+pub enum MoveFolderItemType {
+    MoveApp = 0,
+    MoveView = 1,
+}
+
+impl ::protobuf::ProtobufEnum for MoveFolderItemType {
+    fn value(&self) -> i32 {
+        *self as i32
+    }
+
+    fn from_i32(value: i32) -> ::std::option::Option<MoveFolderItemType> {
+        match value {
+            0 => ::std::option::Option::Some(MoveFolderItemType::MoveApp),
+            1 => ::std::option::Option::Some(MoveFolderItemType::MoveView),
+            _ => ::std::option::Option::None
+        }
+    }
+
+    fn values() -> &'static [Self] {
+        static values: &'static [MoveFolderItemType] = &[
+            MoveFolderItemType::MoveApp,
+            MoveFolderItemType::MoveView,
+        ];
+        values
+    }
+
+    fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            ::protobuf::reflect::EnumDescriptor::new_pb_name::<MoveFolderItemType>("MoveFolderItemType", file_descriptor_proto())
+        })
+    }
+}
+
+impl ::std::marker::Copy for MoveFolderItemType {
+}
+
+impl ::std::default::Default for MoveFolderItemType {
+    fn default() -> Self {
+        MoveFolderItemType::MoveApp
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for MoveFolderItemType {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
+    }
+}
+
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\nview.proto\"\xf5\x02\n\x04View\x12\x0e\n\x02id\x18\x01\x20\x01(\tR\
     \x02id\x12\x20\n\x0cbelong_to_id\x18\x02\x20\x01(\tR\nbelongToId\x12\x12\
@@ -2862,8 +3172,12 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x04name\x18\x02\x20\x01(\tH\0R\x04name\x12\x14\n\x04desc\x18\x03\x20\
     \x01(\tH\x01R\x04desc\x12\x1e\n\tthumbnail\x18\x04\x20\x01(\tH\x02R\tthu\
     mbnailB\r\n\x0bone_of_nameB\r\n\x0bone_of_descB\x12\n\x10one_of_thumbnai\
-    l*'\n\x0cViewDataType\x12\r\n\tTextBlock\x10\0\x12\x08\n\x04Grid\x10\x01\
-    b\x06proto3\
+    l\"y\n\x15MoveFolderItemPayload\x12\x17\n\x07item_id\x18\x01\x20\x01(\tR\
+    \x06itemId\x12\x12\n\x04from\x18\x02\x20\x01(\x05R\x04from\x12\x0e\n\x02\
+    to\x18\x03\x20\x01(\x05R\x02to\x12#\n\x02ty\x18\x04\x20\x01(\x0e2\x13.Mo\
+    veFolderItemTypeR\x02ty*'\n\x0cViewDataType\x12\r\n\tTextBlock\x10\0\x12\
+    \x08\n\x04Grid\x10\x01*/\n\x12MoveFolderItemType\x12\x0b\n\x07MoveApp\
+    \x10\0\x12\x0c\n\x08MoveView\x10\x01b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 10 - 0
shared-lib/flowy-folder-data-model/src/protobuf/proto/view.proto

@@ -54,7 +54,17 @@ message UpdateViewParams {
     oneof one_of_desc { string desc = 3; };
     oneof one_of_thumbnail { string thumbnail = 4; };
 }
+message MoveFolderItemPayload {
+    string item_id = 1;
+    int32 from = 2;
+    int32 to = 3;
+    MoveFolderItemType ty = 4;
+}
 enum ViewDataType {
     TextBlock = 0;
     Grid = 1;
 }
+enum MoveFolderItemType {
+    MoveApp = 0;
+    MoveView = 1;
+}

+ 30 - 0
shared-lib/flowy-sync/src/client_folder/folder_pad.rs

@@ -169,6 +169,21 @@ impl FolderPad {
         })
     }
 
+    #[tracing::instrument(level = "trace", skip(self), err)]
+    pub fn move_app(&mut self, app_id: &str, _from: usize, to: usize) -> CollaborateResult<Option<FolderChange>> {
+        let app = self.read_app(app_id)?;
+        self.with_workspace(&app.workspace_id, |workspace| {
+            match workspace.apps.iter().position(|app| app.id == app_id) {
+                None => Ok(None),
+                Some(index) => {
+                    let app = workspace.apps.remove(index);
+                    workspace.apps.insert(to, app);
+                    Ok(Some(()))
+                }
+            }
+        })
+    }
+
     #[tracing::instrument(level = "trace", skip(self), fields(view_name=%view.name), err)]
     pub fn create_view(&mut self, view: View) -> CollaborateResult<Option<FolderChange>> {
         let app_id = view.belong_to_id.clone();
@@ -235,6 +250,21 @@ impl FolderPad {
         })
     }
 
+    #[tracing::instrument(level = "trace", skip(self), err)]
+    pub fn move_view(&mut self, view_id: &str, _from: usize, to: usize) -> CollaborateResult<Option<FolderChange>> {
+        let view = self.read_view(view_id)?;
+        self.with_app(&view.belong_to_id, |app| {
+            match app.belongings.iter().position(|view| view.id == view_id) {
+                None => Ok(None),
+                Some(index) => {
+                    let view = app.belongings.remove(index);
+                    app.belongings.insert(to, view);
+                    Ok(Some(()))
+                }
+            }
+        })
+    }
+
     pub fn create_trash(&mut self, trash: Vec<Trash>) -> CollaborateResult<Option<FolderChange>> {
         self.with_trash(|t| {
             let mut new_trash = trash.into_iter().map(Arc::new).collect::<Vec<Arc<Trash>>>();

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików