appflowy 3 лет назад
Родитель
Сommit
4966b03123
55 измененных файлов с 866 добавлено и 447 удалено
  1. 21 3
      app_flowy/lib/workspace/application/app/app_bloc.dart
  2. 293 6
      app_flowy/lib/workspace/application/app/app_bloc.freezed.dart
  3. 4 0
      app_flowy/lib/workspace/domain/i_app.dart
  4. 2 4
      app_flowy/lib/workspace/domain/i_workspace.dart
  5. 6 4
      app_flowy/lib/workspace/infrastructure/deps_resolver.dart
  6. 10 0
      app_flowy/lib/workspace/infrastructure/i_app_impl.dart
  7. 1 1
      app_flowy/lib/workspace/infrastructure/i_workspace_impl.dart
  8. 18 2
      app_flowy/lib/workspace/infrastructure/repos/app_repo.dart
  9. 7 20
      app_flowy/lib/workspace/infrastructure/repos/workspace_repo.dart
  10. 29 4
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart
  11. 1 1
      app_flowy/lib/workspace/presentation/widgets/menu/widget/app/menu_app.dart
  12. 1 1
      app_flowy/packages/flowy_sdk/lib/dispatch/code_gen.dart
  13. 5 25
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pb.dart
  14. 2 3
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pbjson.dart
  15. 3 5
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/observable.pbenum.dart
  16. 3 4
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/observable.pbjson.dart
  17. 2 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/trash_create.pbenum.dart
  18. 2 1
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/trash_create.pbjson.dart
  19. 14 0
      backend/src/entities/workspace.rs
  20. 21 9
      backend/src/service/app/app.rs
  21. 7 10
      backend/src/service/trash/trash.rs
  22. 2 2
      backend/src/service/view/view.rs
  23. 2 2
      backend/tests/api/workspace.rs
  24. 1 1
      backend/tests/helper.rs
  25. 0 2
      rust-lib/flowy-derive/src/derive_cache/derive_cache.rs
  26. 1 1
      rust-lib/flowy-test/src/workspace.rs
  27. 0 27
      rust-lib/flowy-workspace/src/entities/app/app_delete.rs
  28. 9 17
      rust-lib/flowy-workspace/src/entities/app/app_query.rs
  29. 1 1
      rust-lib/flowy-workspace/src/entities/app/app_update.rs
  30. 0 2
      rust-lib/flowy-workspace/src/entities/app/mod.rs
  31. 2 0
      rust-lib/flowy-workspace/src/entities/trash/trash_create.rs
  32. 1 1
      rust-lib/flowy-workspace/src/entities/view/view_create.rs
  33. 1 1
      rust-lib/flowy-workspace/src/errors.rs
  34. 1 1
      rust-lib/flowy-workspace/src/event.rs
  35. 23 15
      rust-lib/flowy-workspace/src/handlers/app_handler.rs
  36. 6 1
      rust-lib/flowy-workspace/src/module.rs
  37. 2 3
      rust-lib/flowy-workspace/src/notify/observable.rs
  38. 36 75
      rust-lib/flowy-workspace/src/protobuf/model/app_query.rs
  39. 35 41
      rust-lib/flowy-workspace/src/protobuf/model/observable.rs
  40. 46 41
      rust-lib/flowy-workspace/src/protobuf/model/trash_create.rs
  41. 1 2
      rust-lib/flowy-workspace/src/protobuf/proto/app_query.proto
  42. 2 3
      rust-lib/flowy-workspace/src/protobuf/proto/observable.proto
  43. 1 0
      rust-lib/flowy-workspace/src/protobuf/proto/trash_create.proto
  44. 180 65
      rust-lib/flowy-workspace/src/services/app_controller.rs
  45. 2 2
      rust-lib/flowy-workspace/src/services/server/mod.rs
  46. 3 3
      rust-lib/flowy-workspace/src/services/server/server_api.rs
  47. 2 2
      rust-lib/flowy-workspace/src/services/server/server_api_mock.rs
  48. 14 10
      rust-lib/flowy-workspace/src/services/view_controller.rs
  49. 1 0
      rust-lib/flowy-workspace/src/services/workspace_controller.rs
  50. 1 6
      rust-lib/flowy-workspace/src/sql_tables/app/app_sql.rs
  51. 13 0
      rust-lib/flowy-workspace/src/sql_tables/app/app_table.rs
  52. 4 0
      rust-lib/flowy-workspace/src/sql_tables/trash/trash_table.rs
  53. 2 8
      rust-lib/flowy-workspace/src/sql_tables/view/view_sql.rs
  54. 9 3
      rust-lib/flowy-workspace/tests/workspace/app_test.rs
  55. 10 6
      rust-lib/flowy-workspace/tests/workspace/view_test.rs

+ 21 - 3
app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -1,5 +1,6 @@
 import 'package:app_flowy/workspace/domain/i_app.dart';
 import 'package:flowy_log/flowy_log.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -11,7 +12,7 @@ part 'app_bloc.freezed.dart';
 class AppBloc extends Bloc<AppEvent, AppState> {
   final IApp appManager;
   final IAppListenr listener;
-  AppBloc({required this.appManager, required this.listener}) : super(AppState.initial());
+  AppBloc({required App app, required this.appManager, required this.listener}) : super(AppState.initial(app));
 
   @override
   Stream<AppState> mapEventToState(
@@ -20,7 +21,6 @@ class AppBloc extends Bloc<AppEvent, AppState> {
     yield* event.map(
       initial: (e) async* {
         listener.start(viewsChangeCallback: _handleViewsOrFail);
-
         yield* _fetchViews();
       },
       createView: (CreateView value) async* {
@@ -39,6 +39,20 @@ class AppBloc extends Bloc<AppEvent, AppState> {
       didReceiveViews: (e) async* {
         yield state.copyWith(views: e.views);
       },
+      delete: (e) async* {
+        final result = await appManager.delete();
+        yield result.fold(
+          (l) => state.copyWith(successOrFailure: left(unit)),
+          (error) => state.copyWith(successOrFailure: right(error)),
+        );
+      },
+      rename: (e) async* {
+        final result = await appManager.rename(e.newName);
+        yield result.fold(
+          (l) => state.copyWith(successOrFailure: left(unit)),
+          (error) => state.copyWith(successOrFailure: right(error)),
+        );
+      },
     );
   }
 
@@ -73,19 +87,23 @@ class AppBloc extends Bloc<AppEvent, AppState> {
 class AppEvent with _$AppEvent {
   const factory AppEvent.initial() = Initial;
   const factory AppEvent.createView(String name, String desc, ViewType viewType) = CreateView;
+  const factory AppEvent.delete() = Delete;
+  const factory AppEvent.rename(String newName) = Rename;
   const factory AppEvent.didReceiveViews(List<View> views) = ReceiveViews;
 }
 
 @freezed
 class AppState with _$AppState {
   const factory AppState({
+    required App app,
     required bool isLoading,
     required List<View>? views,
     View? selectedView,
     required Either<Unit, WorkspaceError> successOrFailure,
   }) = _AppState;
 
-  factory AppState.initial() => AppState(
+  factory AppState.initial(App app) => AppState(
+        app: app,
         isLoading: false,
         views: null,
         selectedView: null,

+ 293 - 6
app_flowy/lib/workspace/application/app/app_bloc.freezed.dart

@@ -28,6 +28,16 @@ class _$AppEventTearOff {
     );
   }
 
+  Delete delete() {
+    return const Delete();
+  }
+
+  Rename rename(String newName) {
+    return Rename(
+      newName,
+    );
+  }
+
   ReceiveViews didReceiveViews(List<View> views) {
     return ReceiveViews(
       views,
@@ -45,6 +55,8 @@ mixin _$AppEvent {
     required TResult Function() initial,
     required TResult Function(String name, String desc, ViewType viewType)
         createView,
+    required TResult Function() delete,
+    required TResult Function(String newName) rename,
     required TResult Function(List<View> views) didReceiveViews,
   }) =>
       throw _privateConstructorUsedError;
@@ -52,6 +64,8 @@ mixin _$AppEvent {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
     TResult Function(List<View> views)? didReceiveViews,
     required TResult orElse(),
   }) =>
@@ -60,6 +74,8 @@ mixin _$AppEvent {
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
     required TResult Function(CreateView value) createView,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Rename value) rename,
     required TResult Function(ReceiveViews value) didReceiveViews,
   }) =>
       throw _privateConstructorUsedError;
@@ -67,6 +83,8 @@ mixin _$AppEvent {
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
     TResult Function(ReceiveViews value)? didReceiveViews,
     required TResult orElse(),
   }) =>
@@ -128,6 +146,8 @@ class _$Initial implements Initial {
     required TResult Function() initial,
     required TResult Function(String name, String desc, ViewType viewType)
         createView,
+    required TResult Function() delete,
+    required TResult Function(String newName) rename,
     required TResult Function(List<View> views) didReceiveViews,
   }) {
     return initial();
@@ -138,6 +158,8 @@ class _$Initial implements Initial {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
     TResult Function(List<View> views)? didReceiveViews,
     required TResult orElse(),
   }) {
@@ -152,6 +174,8 @@ class _$Initial implements Initial {
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
     required TResult Function(CreateView value) createView,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Rename value) rename,
     required TResult Function(ReceiveViews value) didReceiveViews,
   }) {
     return initial(this);
@@ -162,6 +186,8 @@ class _$Initial implements Initial {
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
     TResult Function(ReceiveViews value)? didReceiveViews,
     required TResult orElse(),
   }) {
@@ -264,6 +290,8 @@ class _$CreateView implements CreateView {
     required TResult Function() initial,
     required TResult Function(String name, String desc, ViewType viewType)
         createView,
+    required TResult Function() delete,
+    required TResult Function(String newName) rename,
     required TResult Function(List<View> views) didReceiveViews,
   }) {
     return createView(name, desc, viewType);
@@ -274,6 +302,8 @@ class _$CreateView implements CreateView {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
     TResult Function(List<View> views)? didReceiveViews,
     required TResult orElse(),
   }) {
@@ -288,6 +318,8 @@ class _$CreateView implements CreateView {
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
     required TResult Function(CreateView value) createView,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Rename value) rename,
     required TResult Function(ReceiveViews value) didReceiveViews,
   }) {
     return createView(this);
@@ -298,6 +330,8 @@ class _$CreateView implements CreateView {
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
     TResult Function(ReceiveViews value)? didReceiveViews,
     required TResult orElse(),
   }) {
@@ -320,6 +354,227 @@ abstract class CreateView implements AppEvent {
       throw _privateConstructorUsedError;
 }
 
+/// @nodoc
+abstract class $DeleteCopyWith<$Res> {
+  factory $DeleteCopyWith(Delete value, $Res Function(Delete) then) =
+      _$DeleteCopyWithImpl<$Res>;
+}
+
+/// @nodoc
+class _$DeleteCopyWithImpl<$Res> extends _$AppEventCopyWithImpl<$Res>
+    implements $DeleteCopyWith<$Res> {
+  _$DeleteCopyWithImpl(Delete _value, $Res Function(Delete) _then)
+      : super(_value, (v) => _then(v as Delete));
+
+  @override
+  Delete get _value => super._value as Delete;
+}
+
+/// @nodoc
+
+class _$Delete implements Delete {
+  const _$Delete();
+
+  @override
+  String toString() {
+    return 'AppEvent.delete()';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) || (other is Delete);
+  }
+
+  @override
+  int get hashCode => runtimeType.hashCode;
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function(String name, String desc, ViewType viewType)
+        createView,
+    required TResult Function() delete,
+    required TResult Function(String newName) rename,
+    required TResult Function(List<View> views) didReceiveViews,
+  }) {
+    return delete();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    required TResult orElse(),
+  }) {
+    if (delete != null) {
+      return delete();
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(Initial value) initial,
+    required TResult Function(CreateView value) createView,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Rename value) rename,
+    required TResult Function(ReceiveViews value) didReceiveViews,
+  }) {
+    return delete(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    required TResult orElse(),
+  }) {
+    if (delete != null) {
+      return delete(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class Delete implements AppEvent {
+  const factory Delete() = _$Delete;
+}
+
+/// @nodoc
+abstract class $RenameCopyWith<$Res> {
+  factory $RenameCopyWith(Rename value, $Res Function(Rename) then) =
+      _$RenameCopyWithImpl<$Res>;
+  $Res call({String newName});
+}
+
+/// @nodoc
+class _$RenameCopyWithImpl<$Res> extends _$AppEventCopyWithImpl<$Res>
+    implements $RenameCopyWith<$Res> {
+  _$RenameCopyWithImpl(Rename _value, $Res Function(Rename) _then)
+      : super(_value, (v) => _then(v as Rename));
+
+  @override
+  Rename get _value => super._value as Rename;
+
+  @override
+  $Res call({
+    Object? newName = freezed,
+  }) {
+    return _then(Rename(
+      newName == freezed
+          ? _value.newName
+          : newName // ignore: cast_nullable_to_non_nullable
+              as String,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$Rename implements Rename {
+  const _$Rename(this.newName);
+
+  @override
+  final String newName;
+
+  @override
+  String toString() {
+    return 'AppEvent.rename(newName: $newName)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is Rename &&
+            (identical(other.newName, newName) ||
+                const DeepCollectionEquality().equals(other.newName, newName)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^ const DeepCollectionEquality().hash(newName);
+
+  @JsonKey(ignore: true)
+  @override
+  $RenameCopyWith<Rename> get copyWith =>
+      _$RenameCopyWithImpl<Rename>(this, _$identity);
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function(String name, String desc, ViewType viewType)
+        createView,
+    required TResult Function() delete,
+    required TResult Function(String newName) rename,
+    required TResult Function(List<View> views) didReceiveViews,
+  }) {
+    return rename(newName);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
+    TResult Function(List<View> views)? didReceiveViews,
+    required TResult orElse(),
+  }) {
+    if (rename != null) {
+      return rename(newName);
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(Initial value) initial,
+    required TResult Function(CreateView value) createView,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Rename value) rename,
+    required TResult Function(ReceiveViews value) didReceiveViews,
+  }) {
+    return rename(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
+    TResult Function(ReceiveViews value)? didReceiveViews,
+    required TResult orElse(),
+  }) {
+    if (rename != null) {
+      return rename(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class Rename implements AppEvent {
+  const factory Rename(String newName) = _$Rename;
+
+  String get newName => throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $RenameCopyWith<Rename> get copyWith => throw _privateConstructorUsedError;
+}
+
 /// @nodoc
 abstract class $ReceiveViewsCopyWith<$Res> {
   factory $ReceiveViewsCopyWith(
@@ -387,6 +642,8 @@ class _$ReceiveViews implements ReceiveViews {
     required TResult Function() initial,
     required TResult Function(String name, String desc, ViewType viewType)
         createView,
+    required TResult Function() delete,
+    required TResult Function(String newName) rename,
     required TResult Function(List<View> views) didReceiveViews,
   }) {
     return didReceiveViews(views);
@@ -397,6 +654,8 @@ class _$ReceiveViews implements ReceiveViews {
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
     TResult Function(String name, String desc, ViewType viewType)? createView,
+    TResult Function()? delete,
+    TResult Function(String newName)? rename,
     TResult Function(List<View> views)? didReceiveViews,
     required TResult orElse(),
   }) {
@@ -411,6 +670,8 @@ class _$ReceiveViews implements ReceiveViews {
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
     required TResult Function(CreateView value) createView,
+    required TResult Function(Delete value) delete,
+    required TResult Function(Rename value) rename,
     required TResult Function(ReceiveViews value) didReceiveViews,
   }) {
     return didReceiveViews(this);
@@ -421,6 +682,8 @@ class _$ReceiveViews implements ReceiveViews {
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
     TResult Function(CreateView value)? createView,
+    TResult Function(Delete value)? delete,
+    TResult Function(Rename value)? rename,
     TResult Function(ReceiveViews value)? didReceiveViews,
     required TResult orElse(),
   }) {
@@ -445,11 +708,13 @@ class _$AppStateTearOff {
   const _$AppStateTearOff();
 
   _AppState call(
-      {required bool isLoading,
+      {required App app,
+      required bool isLoading,
       required List<View>? views,
       View? selectedView,
       required Either<Unit, WorkspaceError> successOrFailure}) {
     return _AppState(
+      app: app,
       isLoading: isLoading,
       views: views,
       selectedView: selectedView,
@@ -463,6 +728,7 @@ const $AppState = _$AppStateTearOff();
 
 /// @nodoc
 mixin _$AppState {
+  App get app => throw _privateConstructorUsedError;
   bool get isLoading => throw _privateConstructorUsedError;
   List<View>? get views => throw _privateConstructorUsedError;
   View? get selectedView => throw _privateConstructorUsedError;
@@ -479,7 +745,8 @@ abstract class $AppStateCopyWith<$Res> {
   factory $AppStateCopyWith(AppState value, $Res Function(AppState) then) =
       _$AppStateCopyWithImpl<$Res>;
   $Res call(
-      {bool isLoading,
+      {App app,
+      bool isLoading,
       List<View>? views,
       View? selectedView,
       Either<Unit, WorkspaceError> successOrFailure});
@@ -495,12 +762,17 @@ class _$AppStateCopyWithImpl<$Res> implements $AppStateCopyWith<$Res> {
 
   @override
   $Res call({
+    Object? app = freezed,
     Object? isLoading = freezed,
     Object? views = freezed,
     Object? selectedView = freezed,
     Object? successOrFailure = freezed,
   }) {
     return _then(_value.copyWith(
+      app: app == freezed
+          ? _value.app
+          : app // ignore: cast_nullable_to_non_nullable
+              as App,
       isLoading: isLoading == freezed
           ? _value.isLoading
           : isLoading // ignore: cast_nullable_to_non_nullable
@@ -527,7 +799,8 @@ abstract class _$AppStateCopyWith<$Res> implements $AppStateCopyWith<$Res> {
       __$AppStateCopyWithImpl<$Res>;
   @override
   $Res call(
-      {bool isLoading,
+      {App app,
+      bool isLoading,
       List<View>? views,
       View? selectedView,
       Either<Unit, WorkspaceError> successOrFailure});
@@ -544,12 +817,17 @@ class __$AppStateCopyWithImpl<$Res> extends _$AppStateCopyWithImpl<$Res>
 
   @override
   $Res call({
+    Object? app = freezed,
     Object? isLoading = freezed,
     Object? views = freezed,
     Object? selectedView = freezed,
     Object? successOrFailure = freezed,
   }) {
     return _then(_AppState(
+      app: app == freezed
+          ? _value.app
+          : app // ignore: cast_nullable_to_non_nullable
+              as App,
       isLoading: isLoading == freezed
           ? _value.isLoading
           : isLoading // ignore: cast_nullable_to_non_nullable
@@ -574,11 +852,14 @@ class __$AppStateCopyWithImpl<$Res> extends _$AppStateCopyWithImpl<$Res>
 
 class _$_AppState implements _AppState {
   const _$_AppState(
-      {required this.isLoading,
+      {required this.app,
+      required this.isLoading,
       required this.views,
       this.selectedView,
       required this.successOrFailure});
 
+  @override
+  final App app;
   @override
   final bool isLoading;
   @override
@@ -590,13 +871,15 @@ class _$_AppState implements _AppState {
 
   @override
   String toString() {
-    return 'AppState(isLoading: $isLoading, views: $views, selectedView: $selectedView, successOrFailure: $successOrFailure)';
+    return 'AppState(app: $app, isLoading: $isLoading, views: $views, selectedView: $selectedView, successOrFailure: $successOrFailure)';
   }
 
   @override
   bool operator ==(dynamic other) {
     return identical(this, other) ||
         (other is _AppState &&
+            (identical(other.app, app) ||
+                const DeepCollectionEquality().equals(other.app, app)) &&
             (identical(other.isLoading, isLoading) ||
                 const DeepCollectionEquality()
                     .equals(other.isLoading, isLoading)) &&
@@ -613,6 +896,7 @@ class _$_AppState implements _AppState {
   @override
   int get hashCode =>
       runtimeType.hashCode ^
+      const DeepCollectionEquality().hash(app) ^
       const DeepCollectionEquality().hash(isLoading) ^
       const DeepCollectionEquality().hash(views) ^
       const DeepCollectionEquality().hash(selectedView) ^
@@ -626,11 +910,14 @@ class _$_AppState implements _AppState {
 
 abstract class _AppState implements AppState {
   const factory _AppState(
-      {required bool isLoading,
+      {required App app,
+      required bool isLoading,
       required List<View>? views,
       View? selectedView,
       required Either<Unit, WorkspaceError> successOrFailure}) = _$_AppState;
 
+  @override
+  App get app => throw _privateConstructorUsedError;
   @override
   bool get isLoading => throw _privateConstructorUsedError;
   @override

+ 4 - 0
app_flowy/lib/workspace/domain/i_app.dart

@@ -8,6 +8,10 @@ abstract class IApp {
   Future<Either<List<View>, WorkspaceError>> getViews();
 
   Future<Either<View, WorkspaceError>> createView({required String name, String? desc, required ViewType viewType});
+
+  Future<Either<Unit, WorkspaceError>> delete();
+
+  Future<Either<Unit, WorkspaceError>> rename(String newName);
 }
 
 abstract class IAppListenr {

+ 2 - 4
app_flowy/lib/workspace/domain/i_workspace.dart

@@ -1,12 +1,10 @@
 import 'package:flowy_sdk/protobuf/flowy-workspace/protobuf.dart';
 import 'package:dartz/dartz.dart';
 
-typedef WorkspaceCreateAppCallback = void Function(Either<List<App>, WorkspaceError> appsOrFail);
+typedef WorkspaceAppsChangedCallback = void Function(Either<List<App>, WorkspaceError> appsOrFail);
 
 typedef WorkspaceUpdatedCallback = void Function(String name, String desc);
 
-typedef WorkspaceDeleteAppCallback = void Function(Either<List<App>, WorkspaceError> appsOrFail);
-
 abstract class IWorkspace {
   Future<Either<App, WorkspaceError>> createApp({required String name, String? desc});
 
@@ -14,7 +12,7 @@ abstract class IWorkspace {
 }
 
 abstract class IWorkspaceListener {
-  void start({WorkspaceCreateAppCallback? addAppCallback, WorkspaceUpdatedCallback? updatedCallback});
+  void start({WorkspaceAppsChangedCallback? addAppCallback, WorkspaceUpdatedCallback? updatedCallback});
 
   Future<void> stop();
 }

+ 6 - 4
app_flowy/lib/workspace/infrastructure/deps_resolver.dart

@@ -19,6 +19,7 @@ import 'package:app_flowy/workspace/infrastructure/repos/trash_repo.dart';
 import 'package:app_flowy/workspace/infrastructure/repos/view_repo.dart';
 import 'package:app_flowy/workspace/infrastructure/repos/workspace_repo.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
 import 'package:get_it/get_it.dart';
 
@@ -77,10 +78,11 @@ class HomeDepsResolver {
         (user, _) => MenuUserBloc(getIt<IUser>(param1: user), getIt<IUserListener>(param1: user)));
 
     // App
-    getIt.registerFactoryParam<AppBloc, String, void>(
-      (appId, _) => AppBloc(
-        appManager: getIt<IApp>(param1: appId),
-        listener: getIt<IAppListenr>(param1: appId),
+    getIt.registerFactoryParam<AppBloc, App, void>(
+      (app, _) => AppBloc(
+        app: app,
+        appManager: getIt<IApp>(param1: app.id),
+        listener: getIt<IAppListenr>(param1: app.id),
       ),
     );
 

+ 10 - 0
app_flowy/lib/workspace/infrastructure/i_app_impl.dart

@@ -26,6 +26,16 @@ class IAppImpl extends IApp {
       );
     });
   }
+
+  @override
+  Future<Either<Unit, workspace.WorkspaceError>> delete() {
+    return repo.delete();
+  }
+
+  @override
+  Future<Either<Unit, workspace.WorkspaceError>> rename(String newName) {
+    return repo.updateApp(name: newName);
+  }
 }
 
 class IAppListenerhImpl extends IAppListenr {

+ 1 - 1
app_flowy/lib/workspace/infrastructure/i_workspace_impl.dart

@@ -35,7 +35,7 @@ class IWorkspaceListenerImpl extends IWorkspaceListener {
   });
 
   @override
-  void start({WorkspaceCreateAppCallback? addAppCallback, WorkspaceUpdatedCallback? updatedCallback}) {
+  void start({WorkspaceAppsChangedCallback? addAppCallback, WorkspaceUpdatedCallback? updatedCallback}) {
     repo.startListening(createApp: addAppCallback, update: updatedCallback);
   }
 

+ 18 - 2
app_flowy/lib/workspace/infrastructure/repos/app_repo.dart

@@ -6,7 +6,9 @@ import 'package:flowy_log/flowy_log.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/protobuf/flowy-dart-notify/subject.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/app_create.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/app_delete.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/app_query.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/app_update.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/observable.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
@@ -21,7 +23,7 @@ class AppRepository {
   });
 
   Future<Either<App, WorkspaceError>> getAppDesc() {
-    final request = QueryAppRequest.create()..appId = appId;
+    final request = QueryAppRequest.create()..appIds.add(appId);
 
     return WorkspaceEventReadApp(request).send();
   }
@@ -37,7 +39,7 @@ class AppRepository {
   }
 
   Future<Either<List<View>, WorkspaceError>> getViews() {
-    final request = QueryAppRequest.create()..appId = appId;
+    final request = QueryAppRequest.create()..appIds.add(appId);
 
     return WorkspaceEventReadApp(request).send().then((result) {
       return result.fold(
@@ -46,6 +48,20 @@ class AppRepository {
       );
     });
   }
+
+  Future<Either<Unit, WorkspaceError>> delete() {
+    final request = QueryAppRequest.create()..appIds.add(appId);
+    return WorkspaceEventDeleteApp(request).send();
+  }
+
+  Future<Either<Unit, WorkspaceError>> updateApp({String? name}) {
+    UpdateAppRequest request = UpdateAppRequest.create()..appId = appId;
+
+    if (name != null) {
+      request.name = name;
+    }
+    return WorkspaceEventUpdateApp(request).send();
+  }
 }
 
 class AppListenerRepository {

+ 7 - 20
app_flowy/lib/workspace/infrastructure/repos/workspace_repo.dart

@@ -64,8 +64,7 @@ class WorkspaceRepo {
 
 class WorkspaceListenerRepo {
   StreamSubscription<SubscribeObject>? _subscription;
-  WorkspaceCreateAppCallback? _createApp;
-  WorkspaceDeleteAppCallback? _deleteApp;
+  WorkspaceAppsChangedCallback? _appsChanged;
   WorkspaceUpdatedCallback? _update;
   late WorkspaceNotificationParser _parser;
   final UserProfile user;
@@ -77,12 +76,10 @@ class WorkspaceListenerRepo {
   });
 
   void startListening({
-    WorkspaceCreateAppCallback? createApp,
-    WorkspaceDeleteAppCallback? deleteApp,
+    WorkspaceAppsChangedCallback? appsChanged,
     WorkspaceUpdatedCallback? update,
   }) {
-    _createApp = createApp;
-    _deleteApp = deleteApp;
+    _appsChanged = appsChanged;
     _update = update;
 
     _parser = WorkspaceNotificationParser(
@@ -108,23 +105,13 @@ class WorkspaceListenerRepo {
           );
         }
         break;
-      case WorkspaceNotification.WorkspaceCreateApp:
-        if (_createApp != null) {
+      case WorkspaceNotification.WorkspaceAppsChanged:
+        if (_appsChanged != null) {
           result.fold(
-            (payload) => _createApp!(
+            (payload) => _appsChanged!(
               left(RepeatedApp.fromBuffer(payload).items),
             ),
-            (error) => _createApp!(right(error)),
-          );
-        }
-        break;
-      case WorkspaceNotification.WorkspaceDeleteApp:
-        if (_deleteApp != null) {
-          result.fold(
-            (payload) => _deleteApp!(
-              left(RepeatedApp.fromBuffer(payload).items),
-            ),
-            (error) => _deleteApp!(right(error)),
+            (error) => _appsChanged!(right(error)),
           );
         }
         break;

+ 29 - 4
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart

@@ -1,3 +1,5 @@
+import 'package:app_flowy/workspace/domain/edit_action/app_edit.dart';
+import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_infra/flowy_icon_data_icons.dart';
 import 'package:flowy_infra/theme.dart';
@@ -10,6 +12,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'package:app_flowy/workspace/application/app/app_bloc.dart';
 import 'package:styled_widget/styled_widget.dart';
+import 'package:dartz/dartz.dart';
 
 import '../menu_app.dart';
 import 'add_button.dart';
@@ -67,13 +70,15 @@ class MenuAppHeader extends StatelessWidget {
       child: GestureDetector(
         behavior: HitTestBehavior.opaque,
         onTap: () {
-          // Open the document
           ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle();
         },
         onSecondaryTap: () {
-          AppDisclosureActions(onSelected: (action) {
-            print(action);
-          }).show(context, context, anchorDirection: AnchorDirection.bottomWithCenterAligned);
+          final actionList = AppDisclosureActions(onSelected: (action) => _handleAction(context, action));
+          actionList.show(
+            context,
+            context,
+            anchorDirection: AnchorDirection.bottomWithCenterAligned,
+          );
         },
         child: FlowyText.medium(
           app.name,
@@ -90,4 +95,24 @@ class MenuAppHeader extends StatelessWidget {
       },
     ).padding(right: MenuAppSizes.headerPadding);
   }
+
+  void _handleAction(BuildContext context, Option<AppDisclosureAction> action) {
+    action.fold(() {}, (action) {
+      switch (action) {
+        case AppDisclosureAction.rename:
+          TextFieldDialog(
+            title: 'Rename',
+            value: context.read<AppBloc>().state.app.name,
+            confirm: (newValue) {
+              context.read<AppBloc>().add(AppEvent.rename(newValue));
+            },
+          ).show(context);
+
+          break;
+        case AppDisclosureAction.delete:
+          context.read<AppBloc>().add(const AppEvent.delete());
+          break;
+      }
+    });
+  }
 }

+ 1 - 1
app_flowy/lib/workspace/presentation/widgets/menu/widget/app/menu_app.dart

@@ -23,7 +23,7 @@ class MenuApp extends MenuItem {
       providers: [
         BlocProvider<AppBloc>(
           create: (context) {
-            final appBloc = getIt<AppBloc>(param1: app.id);
+            final appBloc = getIt<AppBloc>(param1: app);
             appBloc.add(const AppEvent.initial());
             return appBloc;
           },

+ 1 - 1
app_flowy/packages/flowy_sdk/lib/dispatch/code_gen.dart

@@ -119,7 +119,7 @@ class WorkspaceEventCreateApp {
 }
 
 class WorkspaceEventDeleteApp {
-     DeleteAppRequest request;
+     QueryAppRequest request;
      WorkspaceEventDeleteApp(this.request);
 
     Future<Either<Unit, WorkspaceError>> send() {

+ 5 - 25
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pb.dart

@@ -11,22 +11,17 @@ import 'package:protobuf/protobuf.dart' as $pb;
 
 class QueryAppRequest extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'QueryAppRequest', createEmptyInstance: create)
-    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appId')
-    ..aOB(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isTrash')
+    ..pPS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appIds')
     ..hasRequiredFields = false
   ;
 
   QueryAppRequest._() : super();
   factory QueryAppRequest({
-    $core.String? appId,
-    $core.bool? isTrash,
+    $core.Iterable<$core.String>? appIds,
   }) {
     final _result = create();
-    if (appId != null) {
-      _result.appId = appId;
-    }
-    if (isTrash != null) {
-      _result.isTrash = isTrash;
+    if (appIds != null) {
+      _result.appIds.addAll(appIds);
     }
     return _result;
   }
@@ -52,22 +47,7 @@ class QueryAppRequest extends $pb.GeneratedMessage {
   static QueryAppRequest? _defaultInstance;
 
   @$pb.TagNumber(1)
-  $core.String get appId => $_getSZ(0);
-  @$pb.TagNumber(1)
-  set appId($core.String v) { $_setString(0, v); }
-  @$pb.TagNumber(1)
-  $core.bool hasAppId() => $_has(0);
-  @$pb.TagNumber(1)
-  void clearAppId() => clearField(1);
-
-  @$pb.TagNumber(2)
-  $core.bool get isTrash => $_getBF(1);
-  @$pb.TagNumber(2)
-  set isTrash($core.bool v) { $_setBool(1, v); }
-  @$pb.TagNumber(2)
-  $core.bool hasIsTrash() => $_has(1);
-  @$pb.TagNumber(2)
-  void clearIsTrash() => clearField(2);
+  $core.List<$core.String> get appIds => $_getList(0);
 }
 
 class AppIdentifier extends $pb.GeneratedMessage {

+ 2 - 3
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/app_query.pbjson.dart

@@ -12,13 +12,12 @@ import 'dart:typed_data' as $typed_data;
 const QueryAppRequest$json = const {
   '1': 'QueryAppRequest',
   '2': const [
-    const {'1': 'app_id', '3': 1, '4': 1, '5': 9, '10': 'appId'},
-    const {'1': 'is_trash', '3': 2, '4': 1, '5': 8, '10': 'isTrash'},
+    const {'1': 'app_ids', '3': 1, '4': 3, '5': 9, '10': 'appIds'},
   ],
 };
 
 /// Descriptor for `QueryAppRequest`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List queryAppRequestDescriptor = $convert.base64Decode('Cg9RdWVyeUFwcFJlcXVlc3QSFQoGYXBwX2lkGAEgASgJUgVhcHBJZBIZCghpc190cmFzaBgCIAEoCFIHaXNUcmFzaA==');
+final $typed_data.Uint8List queryAppRequestDescriptor = $convert.base64Decode('Cg9RdWVyeUFwcFJlcXVlc3QSFwoHYXBwX2lkcxgBIAMoCVIGYXBwSWRz');
 @$core.Deprecated('Use appIdentifierDescriptor instead')
 const AppIdentifier$json = const {
   '1': 'AppIdentifier',

+ 3 - 5
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/observable.pbenum.dart

@@ -14,9 +14,8 @@ class WorkspaceNotification extends $pb.ProtobufEnum {
   static const WorkspaceNotification UserCreateWorkspace = WorkspaceNotification._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UserCreateWorkspace');
   static const WorkspaceNotification UserDeleteWorkspace = WorkspaceNotification._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UserDeleteWorkspace');
   static const WorkspaceNotification WorkspaceUpdated = WorkspaceNotification._(12, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceUpdated');
-  static const WorkspaceNotification WorkspaceCreateApp = WorkspaceNotification._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceCreateApp');
-  static const WorkspaceNotification WorkspaceDeleteApp = WorkspaceNotification._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceDeleteApp');
-  static const WorkspaceNotification WorkspaceListUpdated = WorkspaceNotification._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceListUpdated');
+  static const WorkspaceNotification WorkspaceListUpdated = WorkspaceNotification._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceListUpdated');
+  static const WorkspaceNotification WorkspaceAppsChanged = WorkspaceNotification._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'WorkspaceAppsChanged');
   static const WorkspaceNotification AppUpdated = WorkspaceNotification._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AppUpdated');
   static const WorkspaceNotification AppViewsChanged = WorkspaceNotification._(24, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AppViewsChanged');
   static const WorkspaceNotification ViewUpdated = WorkspaceNotification._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ViewUpdated');
@@ -28,9 +27,8 @@ class WorkspaceNotification extends $pb.ProtobufEnum {
     UserCreateWorkspace,
     UserDeleteWorkspace,
     WorkspaceUpdated,
-    WorkspaceCreateApp,
-    WorkspaceDeleteApp,
     WorkspaceListUpdated,
+    WorkspaceAppsChanged,
     AppUpdated,
     AppViewsChanged,
     ViewUpdated,

+ 3 - 4
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/observable.pbjson.dart

@@ -16,9 +16,8 @@ const WorkspaceNotification$json = const {
     const {'1': 'UserCreateWorkspace', '2': 10},
     const {'1': 'UserDeleteWorkspace', '2': 11},
     const {'1': 'WorkspaceUpdated', '2': 12},
-    const {'1': 'WorkspaceCreateApp', '2': 13},
-    const {'1': 'WorkspaceDeleteApp', '2': 14},
-    const {'1': 'WorkspaceListUpdated', '2': 15},
+    const {'1': 'WorkspaceListUpdated', '2': 13},
+    const {'1': 'WorkspaceAppsChanged', '2': 14},
     const {'1': 'AppUpdated', '2': 21},
     const {'1': 'AppViewsChanged', '2': 24},
     const {'1': 'ViewUpdated', '2': 31},
@@ -28,4 +27,4 @@ const WorkspaceNotification$json = const {
 };
 
 /// Descriptor for `WorkspaceNotification`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List workspaceNotificationDescriptor = $convert.base64Decode('ChVXb3Jrc3BhY2VOb3RpZmljYXRpb24SCwoHVW5rbm93bhAAEhcKE1VzZXJDcmVhdGVXb3Jrc3BhY2UQChIXChNVc2VyRGVsZXRlV29ya3NwYWNlEAsSFAoQV29ya3NwYWNlVXBkYXRlZBAMEhYKEldvcmtzcGFjZUNyZWF0ZUFwcBANEhYKEldvcmtzcGFjZURlbGV0ZUFwcBAOEhgKFFdvcmtzcGFjZUxpc3RVcGRhdGVkEA8SDgoKQXBwVXBkYXRlZBAVEhMKD0FwcFZpZXdzQ2hhbmdlZBAYEg8KC1ZpZXdVcGRhdGVkEB8SFAoQVXNlclVuYXV0aG9yaXplZBBkEhEKDFRyYXNoVXBkYXRlZBDoBw==');
+final $typed_data.Uint8List workspaceNotificationDescriptor = $convert.base64Decode('ChVXb3Jrc3BhY2VOb3RpZmljYXRpb24SCwoHVW5rbm93bhAAEhcKE1VzZXJDcmVhdGVXb3Jrc3BhY2UQChIXChNVc2VyRGVsZXRlV29ya3NwYWNlEAsSFAoQV29ya3NwYWNlVXBkYXRlZBAMEhgKFFdvcmtzcGFjZUxpc3RVcGRhdGVkEA0SGAoUV29ya3NwYWNlQXBwc0NoYW5nZWQQDhIOCgpBcHBVcGRhdGVkEBUSEwoPQXBwVmlld3NDaGFuZ2VkEBgSDwoLVmlld1VwZGF0ZWQQHxIUChBVc2VyVW5hdXRob3JpemVkEGQSEQoMVHJhc2hVcGRhdGVkEOgH');

+ 2 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/trash_create.pbenum.dart

@@ -12,10 +12,12 @@ import 'package:protobuf/protobuf.dart' as $pb;
 class TrashType extends $pb.ProtobufEnum {
   static const TrashType Unknown = TrashType._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Unknown');
   static const TrashType View = TrashType._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'View');
+  static const TrashType App = TrashType._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'App');
 
   static const $core.List<TrashType> values = <TrashType> [
     Unknown,
     View,
+    App,
   ];
 
   static final $core.Map<$core.int, TrashType> _byValue = $pb.ProtobufEnum.initByValue(values);

+ 2 - 1
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/trash_create.pbjson.dart

@@ -14,11 +14,12 @@ const TrashType$json = const {
   '2': const [
     const {'1': 'Unknown', '2': 0},
     const {'1': 'View', '2': 1},
+    const {'1': 'App', '2': 2},
   ],
 };
 
 /// Descriptor for `TrashType`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List trashTypeDescriptor = $convert.base64Decode('CglUcmFzaFR5cGUSCwoHVW5rbm93bhAAEggKBFZpZXcQAQ==');
+final $typed_data.Uint8List trashTypeDescriptor = $convert.base64Decode('CglUcmFzaFR5cGUSCwoHVW5rbm93bhAAEggKBFZpZXcQARIHCgNBcHAQAg==');
 @$core.Deprecated('Use trashIdentifiersDescriptor instead')
 const TrashIdentifiers$json = const {
   '1': 'TrashIdentifiers',

+ 14 - 0
backend/src/entities/workspace.rs

@@ -46,6 +46,20 @@ impl std::convert::Into<App> for AppTable {
     }
 }
 
+impl std::convert::Into<Trash> for AppTable {
+    fn into(self) -> Trash {
+        Trash {
+            id: self.id.to_string(),
+            name: self.name,
+            modified_time: self.modified_time.timestamp(),
+            create_time: self.create_time.timestamp(),
+            ty: TrashType::App,
+            unknown_fields: Default::default(),
+            cached_size: Default::default(),
+        }
+    }
+}
+
 #[derive(Debug, Clone, sqlx::FromRow)]
 pub struct ViewTable {
     pub(crate) id: uuid::Uuid,

+ 21 - 9
backend/src/service/app/app.rs

@@ -4,6 +4,7 @@ use crate::{
     sqlx_ext::{map_sqlx_error, DBTransaction, SqlBuilder},
 };
 
+use crate::service::trash::read_trash_ids;
 use chrono::Utc;
 use flowy_net::errors::{invalid_params, ServerError};
 use flowy_workspace::{
@@ -44,19 +45,16 @@ pub(crate) async fn read_app(
     app_id: Uuid,
     user: &LoggedUser,
 ) -> Result<App, ServerError> {
-    let (sql, args) = SqlBuilder::select(APP_TABLE)
-        .add_field("*")
-        .and_where_eq("id", app_id)
-        .build()?;
+    let table = read_app_table(app_id, transaction).await?;
 
-    let table = sqlx::query_as_with::<Postgres, AppTable, PgArguments>(&sql, args)
-        .fetch_one(transaction as &mut DBTransaction<'_>)
-        .await
-        .map_err(map_sqlx_error)?;
+    let read_trash_ids = read_trash_ids(user, transaction).await?;
+    if read_trash_ids.contains(&table.id.to_string()) {
+        return Err(ServerError::record_not_found());
+    }
 
     let mut views = RepeatedView::default();
     views.set_items(
-        read_view_belong_to_id(user, transaction as &mut DBTransaction<'_>, &table.id.to_string())
+        read_view_belong_to_id(&table.id.to_string(), user, transaction as &mut DBTransaction<'_>)
             .await?
             .into(),
     );
@@ -66,6 +64,20 @@ pub(crate) async fn read_app(
     Ok(app)
 }
 
+pub(crate) async fn read_app_table(app_id: Uuid, transaction: &mut DBTransaction<'_>) -> Result<AppTable, ServerError> {
+    let (sql, args) = SqlBuilder::select(APP_TABLE)
+        .add_field("*")
+        .and_where_eq("id", app_id)
+        .build()?;
+
+    let table = sqlx::query_as_with::<Postgres, AppTable, PgArguments>(&sql, args)
+        .fetch_one(transaction as &mut DBTransaction<'_>)
+        .await
+        .map_err(map_sqlx_error)?;
+
+    Ok(table)
+}
+
 pub(crate) async fn update_app(
     transaction: &mut DBTransaction<'_>,
     app_id: Uuid,

+ 7 - 10
backend/src/service/trash/trash.rs

@@ -1,6 +1,7 @@
 use crate::{
     entities::workspace::{TrashTable, TRASH_TABLE},
     service::{
+        app::app::{delete_app, read_app_table},
         user::LoggedUser,
         view::{delete_view, read_view_table},
     },
@@ -82,16 +83,6 @@ pub(crate) async fn delete_trash(
             .await
             .map_err(map_sqlx_error)?;
 
-        match TrashType::from_i32(trash_table.ty) {
-            None => log::error!("Parser trash type with value: {} failed", trash_table.ty),
-            Some(ty) => match ty {
-                TrashType::Unknown => {},
-                TrashType::View => {
-                    let _ = delete_view(transaction as &mut DBTransaction<'_>, vec![trash_table.id]).await;
-                },
-            },
-        }
-
         let _ = delete_trash_targets(
             transaction as &mut DBTransaction<'_>,
             vec![(trash_table.id.clone(), trash_table.ty)],
@@ -120,6 +111,9 @@ async fn delete_trash_targets(
                 TrashType::View => {
                     let _ = delete_view(transaction as &mut DBTransaction<'_>, vec![id]).await;
                 },
+                TrashType::App => {
+                    let _ = delete_app(transaction as &mut DBTransaction<'_>, id).await;
+                },
             },
         }
     }
@@ -163,6 +157,9 @@ pub(crate) async fn read_trash(
                 TrashType::View => {
                     trash.push(read_view_table(table.id, transaction).await?.into());
                 },
+                TrashType::App => {
+                    trash.push(read_app_table(table.id, transaction).await?.into());
+                },
             },
         }
     }

+ 2 - 2
backend/src/service/view/view.rs

@@ -100,7 +100,7 @@ pub(crate) async fn read_view(
 
     let mut views = RepeatedView::default();
     views.set_items(
-        read_view_belong_to_id(&user, transaction, &table.id.to_string())
+        read_view_belong_to_id(&table.id.to_string(), &user, transaction)
             .await?
             .into(),
     );
@@ -128,9 +128,9 @@ pub(crate) async fn read_view_table(
 
 // transaction must be commit from caller
 pub(crate) async fn read_view_belong_to_id<'c>(
+    id: &str,
     user: &LoggedUser,
     transaction: &mut DBTransaction<'_>,
-    id: &str,
 ) -> Result<Vec<View>, ServerError> {
     // TODO: add index for app_table
     let (sql, args) = SqlBuilder::select(VIEW_TABLE)

+ 2 - 2
backend/tests/api/workspace.rs

@@ -1,6 +1,6 @@
 use crate::helper::*;
 use flowy_workspace::entities::{
-    app::{AppIdentifier, DeleteAppParams, UpdateAppParams},
+    app::{AppIdentifier, UpdateAppParams},
     trash::{TrashIdentifier, TrashIdentifiers, TrashType},
     view::{UpdateViewParams, ViewIdentifier},
     workspace::{CreateWorkspaceParams, DeleteWorkspaceParams, QueryWorkspaceParams, UpdateWorkspaceParams},
@@ -123,7 +123,7 @@ async fn app_update() {
 async fn app_delete() {
     let test = AppTest::new().await;
 
-    let delete_params = DeleteAppParams {
+    let delete_params = AppIdentifier {
         app_id: test.app.id.clone(),
     };
     test.server.delete_app(delete_params).await;

+ 1 - 1
backend/tests/helper.rs

@@ -95,7 +95,7 @@ impl TestUserServer {
         update_app_request(self.user_token(), params, &url).await.unwrap();
     }
 
-    pub async fn delete_app(&self, params: DeleteAppParams) {
+    pub async fn delete_app(&self, params: AppIdentifier) {
         let url = format!("{}/api/app", self.http_addr());
         delete_app_request(self.user_token(), params, &url).await.unwrap();
     }

+ 0 - 2
rust-lib/flowy-derive/src/derive_cache/derive_cache.rs

@@ -25,8 +25,6 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
         | "RepeatedApp"
         | "UpdateAppRequest"
         | "UpdateAppParams"
-        | "DeleteAppRequest"
-        | "DeleteAppParams"
         | "UpdateWorkspaceRequest"
         | "UpdateWorkspaceParams"
         | "DeleteWorkspaceRequest"

+ 1 - 1
rust-lib/flowy-test/src/workspace.rs

@@ -172,7 +172,7 @@ pub async fn create_app(sdk: &FlowyTestSDK, name: &str, desc: &str, workspace_id
 }
 
 pub async fn delete_app(sdk: &FlowyTestSDK, app_id: &str) {
-    let delete_app_request = DeleteAppRequest {
+    let delete_app_request = AppIdentifier {
         app_id: app_id.to_string(),
     };
 

+ 0 - 27
rust-lib/flowy-workspace/src/entities/app/app_delete.rs

@@ -1,27 +0,0 @@
-use crate::{entities::app::parser::AppId, errors::WorkspaceError};
-use flowy_derive::ProtoBuf;
-use std::convert::TryInto;
-
-#[derive(Default, ProtoBuf)]
-pub struct DeleteAppRequest {
-    #[pb(index = 1)]
-    pub app_id: String,
-}
-
-#[derive(Default, ProtoBuf, Clone)]
-pub struct DeleteAppParams {
-    #[pb(index = 1)]
-    pub app_id: String,
-}
-
-impl TryInto<DeleteAppParams> for DeleteAppRequest {
-    type Error = WorkspaceError;
-
-    fn try_into(self) -> Result<DeleteAppParams, Self::Error> {
-        let app_id = AppId::parse(self.app_id)
-            .map_err(|e| WorkspaceError::app_id().context(e))?
-            .0;
-
-        Ok(DeleteAppParams { app_id })
-    }
-}

+ 9 - 17
rust-lib/flowy-workspace/src/entities/app/app_query.rs

@@ -5,19 +5,7 @@ use std::convert::TryInto;
 #[derive(Default, ProtoBuf, Clone)]
 pub struct QueryAppRequest {
     #[pb(index = 1)]
-    pub app_id: String,
-
-    #[pb(index = 2)]
-    pub is_trash: bool,
-}
-
-impl QueryAppRequest {
-    pub fn new(app_id: &str) -> Self {
-        QueryAppRequest {
-            app_id: app_id.to_string(),
-            is_trash: false,
-        }
-    }
+    pub app_ids: Vec<String>,
 }
 
 #[derive(ProtoBuf, Default, Clone, Debug)]
@@ -30,7 +18,6 @@ impl AppIdentifier {
     pub fn new(app_id: &str) -> Self {
         Self {
             app_id: app_id.to_string(),
-            ..Default::default()
         }
     }
 }
@@ -39,10 +26,15 @@ impl TryInto<AppIdentifier> for QueryAppRequest {
     type Error = WorkspaceError;
 
     fn try_into(self) -> Result<AppIdentifier, Self::Error> {
-        let app_id = AppId::parse(self.app_id)
-            .map_err(|e| WorkspaceError::app_id().context(e))?
-            .0;
+        debug_assert!(self.app_ids.len() == 1);
+        if self.app_ids.len() != 1 {
+            return Err(WorkspaceError::invalid_view_id().context("The len of app_ids should be equal to 1"));
+        }
 
+        let app_id = self.app_ids.first().unwrap().clone();
+        let app_id = AppId::parse(app_id)
+            .map_err(|e| WorkspaceError::invalid_app_id().context(e))?
+            .0;
         Ok(AppIdentifier { app_id })
     }
 }

+ 1 - 1
rust-lib/flowy-workspace/src/entities/app/app_update.rs

@@ -73,7 +73,7 @@ impl TryInto<UpdateAppParams> for UpdateAppRequest {
 
     fn try_into(self) -> Result<UpdateAppParams, Self::Error> {
         let app_id = AppId::parse(self.app_id)
-            .map_err(|e| WorkspaceError::app_id().context(e))?
+            .map_err(|e| WorkspaceError::invalid_app_id().context(e))?
             .0;
 
         let name = match self.name {

+ 0 - 2
rust-lib/flowy-workspace/src/entities/app/mod.rs

@@ -2,10 +2,8 @@ pub use app_create::*;
 pub use app_update::*;
 
 mod app_create;
-mod app_delete;
 mod app_query;
 mod app_update;
 pub mod parser;
 
-pub use app_delete::*;
 pub use app_query::*;

+ 2 - 0
rust-lib/flowy-workspace/src/entities/trash/trash_create.rs

@@ -6,6 +6,7 @@ use std::fmt::Formatter;
 pub enum TrashType {
     Unknown = 0,
     View    = 1,
+    App     = 2,
 }
 
 impl std::convert::TryFrom<i32> for TrashType {
@@ -15,6 +16,7 @@ impl std::convert::TryFrom<i32> for TrashType {
         match value {
             0 => Ok(TrashType::Unknown),
             1 => Ok(TrashType::View),
+            2 => Ok(TrashType::App),
             _ => Err(format!("Invalid trash type: {}", value)),
         }
     }

+ 1 - 1
rust-lib/flowy-workspace/src/entities/view/view_create.rs

@@ -98,7 +98,7 @@ impl TryInto<CreateViewParams> for CreateViewRequest {
             .0;
 
         let belong_to_id = AppId::parse(self.belong_to_id)
-            .map_err(|e| WorkspaceError::app_id().context(e))?
+            .map_err(|e| WorkspaceError::invalid_app_id().context(e))?
             .0;
 
         let thumbnail = match self.thumbnail {

+ 1 - 1
rust-lib/flowy-workspace/src/errors.rs

@@ -42,7 +42,7 @@ impl WorkspaceError {
     static_workspace_error!(color_style, ErrorCode::AppColorStyleInvalid);
     static_workspace_error!(workspace_desc, ErrorCode::WorkspaceDescInvalid);
     static_workspace_error!(app_name, ErrorCode::AppNameInvalid);
-    static_workspace_error!(app_id, ErrorCode::AppIdInvalid);
+    static_workspace_error!(invalid_app_id, ErrorCode::AppIdInvalid);
     static_workspace_error!(view_name, ErrorCode::ViewNameInvalid);
     static_workspace_error!(view_thumbnail, ErrorCode::ViewThumbnailInvalid);
     static_workspace_error!(invalid_view_id, ErrorCode::ViewIdInvalid);

+ 1 - 1
rust-lib/flowy-workspace/src/event.rs

@@ -25,7 +25,7 @@ pub enum WorkspaceEvent {
     #[event(input = "CreateAppRequest", output = "App")]
     CreateApp         = 101,
 
-    #[event(input = "DeleteAppRequest")]
+    #[event(input = "QueryAppRequest")]
     DeleteApp         = 102,
 
     #[event(input = "QueryAppRequest", output = "App")]

+ 23 - 15
rust-lib/flowy-workspace/src/handlers/app_handler.rs

@@ -1,17 +1,18 @@
 use crate::{
-    entities::app::{
-        App,
-        AppIdentifier,
-        CreateAppParams,
-        CreateAppRequest,
-        DeleteAppParams,
-        DeleteAppRequest,
-        QueryAppRequest,
-        UpdateAppParams,
-        UpdateAppRequest,
+    entities::{
+        app::{
+            App,
+            AppIdentifier,
+            CreateAppParams,
+            CreateAppRequest,
+            QueryAppRequest,
+            UpdateAppParams,
+            UpdateAppRequest,
+        },
+        trash::Trash,
     },
     errors::WorkspaceError,
-    services::{AppController, ViewController},
+    services::{AppController, TrashCan, ViewController},
 };
 use flowy_dispatch::prelude::{data_result, Data, DataResult, Unit};
 use std::{convert::TryInto, sync::Arc};
@@ -27,13 +28,20 @@ pub(crate) async fn create_app_handler(
     data_result(detail)
 }
 
-#[tracing::instrument(skip(data, controller))]
+#[tracing::instrument(skip(data, controller, trash_can))]
 pub(crate) async fn delete_app_handler(
-    data: Data<DeleteAppRequest>,
+    data: Data<QueryAppRequest>,
     controller: Unit<Arc<AppController>>,
+    trash_can: Unit<Arc<TrashCan>>,
 ) -> Result<(), WorkspaceError> {
-    let params: DeleteAppParams = data.into_inner().try_into()?;
-    let _ = controller.delete_app(&params.app_id).await?;
+    let params: AppIdentifier = data.into_inner().try_into()?;
+    let trash = controller
+        .read_app_tables(vec![params.app_id])?
+        .into_iter()
+        .map(|view_table| view_table.into())
+        .collect::<Vec<Trash>>();
+
+    let _ = trash_can.add(trash).await?;
     Ok(())
 }
 

+ 6 - 1
rust-lib/flowy-workspace/src/module.rs

@@ -48,7 +48,12 @@ pub fn mk_workspace(
         flowy_document,
     ));
 
-    let app_controller = Arc::new(AppController::new(user.clone(), database.clone(), server.clone()));
+    let app_controller = Arc::new(AppController::new(
+        user.clone(),
+        database.clone(),
+        trash_can.clone(),
+        server.clone(),
+    ));
 
     let workspace_controller = Arc::new(WorkspaceController::new(
         user.clone(),

+ 2 - 3
rust-lib/flowy-workspace/src/notify/observable.rs

@@ -8,9 +8,8 @@ pub(crate) enum WorkspaceNotification {
     UserCreateWorkspace  = 10,
     UserDeleteWorkspace  = 11,
     WorkspaceUpdated     = 12,
-    WorkspaceCreateApp   = 13,
-    WorkspaceDeleteApp   = 14,
-    WorkspaceListUpdated = 15,
+    WorkspaceListUpdated = 13,
+    WorkspaceAppsChanged = 14,
     AppUpdated           = 21,
     AppViewsChanged      = 24,
     ViewUpdated          = 31,

+ 36 - 75
rust-lib/flowy-workspace/src/protobuf/model/app_query.rs

@@ -26,8 +26,7 @@
 #[derive(PartialEq,Clone,Default)]
 pub struct QueryAppRequest {
     // message fields
-    pub app_id: ::std::string::String,
-    pub is_trash: bool,
+    pub app_ids: ::protobuf::RepeatedField<::std::string::String>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -44,45 +43,29 @@ impl QueryAppRequest {
         ::std::default::Default::default()
     }
 
-    // string app_id = 1;
+    // repeated string app_ids = 1;
 
 
-    pub fn get_app_id(&self) -> &str {
-        &self.app_id
+    pub fn get_app_ids(&self) -> &[::std::string::String] {
+        &self.app_ids
     }
-    pub fn clear_app_id(&mut self) {
-        self.app_id.clear();
+    pub fn clear_app_ids(&mut self) {
+        self.app_ids.clear();
     }
 
     // Param is passed by value, moved
-    pub fn set_app_id(&mut self, v: ::std::string::String) {
-        self.app_id = v;
+    pub fn set_app_ids(&mut self, v: ::protobuf::RepeatedField<::std::string::String>) {
+        self.app_ids = v;
     }
 
     // Mutable pointer to the field.
-    // If field is not initialized, it is initialized with default value first.
-    pub fn mut_app_id(&mut self) -> &mut ::std::string::String {
-        &mut self.app_id
+    pub fn mut_app_ids(&mut self) -> &mut ::protobuf::RepeatedField<::std::string::String> {
+        &mut self.app_ids
     }
 
     // Take field
-    pub fn take_app_id(&mut self) -> ::std::string::String {
-        ::std::mem::replace(&mut self.app_id, ::std::string::String::new())
-    }
-
-    // bool is_trash = 2;
-
-
-    pub fn get_is_trash(&self) -> bool {
-        self.is_trash
-    }
-    pub fn clear_is_trash(&mut self) {
-        self.is_trash = false;
-    }
-
-    // Param is passed by value, moved
-    pub fn set_is_trash(&mut self, v: bool) {
-        self.is_trash = v;
+    pub fn take_app_ids(&mut self) -> ::protobuf::RepeatedField<::std::string::String> {
+        ::std::mem::replace(&mut self.app_ids, ::protobuf::RepeatedField::new())
     }
 }
 
@@ -96,14 +79,7 @@ impl ::protobuf::Message for QueryAppRequest {
             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.app_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_bool()?;
-                    self.is_trash = tmp;
+                    ::protobuf::rt::read_repeated_string_into(wire_type, is, &mut self.app_ids)?;
                 },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
@@ -117,24 +93,18 @@ impl ::protobuf::Message for QueryAppRequest {
     #[allow(unused_variables)]
     fn compute_size(&self) -> u32 {
         let mut my_size = 0;
-        if !self.app_id.is_empty() {
-            my_size += ::protobuf::rt::string_size(1, &self.app_id);
-        }
-        if self.is_trash != false {
-            my_size += 2;
-        }
+        for value in &self.app_ids {
+            my_size += ::protobuf::rt::string_size(1, &value);
+        };
         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.app_id.is_empty() {
-            os.write_string(1, &self.app_id)?;
-        }
-        if self.is_trash != false {
-            os.write_bool(2, self.is_trash)?;
-        }
+        for v in &self.app_ids {
+            os.write_string(1, &v)?;
+        };
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -173,15 +143,10 @@ impl ::protobuf::Message for QueryAppRequest {
         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>(
-                "app_id",
-                |m: &QueryAppRequest| { &m.app_id },
-                |m: &mut QueryAppRequest| { &mut m.app_id },
-            ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
-                "is_trash",
-                |m: &QueryAppRequest| { &m.is_trash },
-                |m: &mut QueryAppRequest| { &mut m.is_trash },
+            fields.push(::protobuf::reflect::accessor::make_repeated_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "app_ids",
+                |m: &QueryAppRequest| { &m.app_ids },
+                |m: &mut QueryAppRequest| { &mut m.app_ids },
             ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<QueryAppRequest>(
                 "QueryAppRequest",
@@ -199,8 +164,7 @@ impl ::protobuf::Message for QueryAppRequest {
 
 impl ::protobuf::Clear for QueryAppRequest {
     fn clear(&mut self) {
-        self.app_id.clear();
-        self.is_trash = false;
+        self.app_ids.clear();
         self.unknown_fields.clear();
     }
 }
@@ -377,21 +341,18 @@ impl ::protobuf::reflect::ProtobufValue for AppIdentifier {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0fapp_query.proto\"C\n\x0fQueryAppRequest\x12\x15\n\x06app_id\x18\
-    \x01\x20\x01(\tR\x05appId\x12\x19\n\x08is_trash\x18\x02\x20\x01(\x08R\
-    \x07isTrash\"&\n\rAppIdentifier\x12\x15\n\x06app_id\x18\x01\x20\x01(\tR\
-    \x05appIdJ\xe7\x01\n\x06\x12\x04\0\0\x08\x01\n\x08\n\x01\x0c\x12\x03\0\0\
-    \x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\n\x03\x04\0\x01\x12\x03\
-    \x02\x08\x17\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x16\n\x0c\n\x05\x04\
-    \0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\
-    \x0b\x11\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x14\x15\n\x0b\n\x04\x04\
-    \0\x02\x01\x12\x03\x04\x04\x16\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\
-    \x04\x08\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\t\x11\n\x0c\n\x05\x04\
-    \0\x02\x01\x03\x12\x03\x04\x14\x15\n\n\n\x02\x04\x01\x12\x04\x06\0\x08\
-    \x01\n\n\n\x03\x04\x01\x01\x12\x03\x06\x08\x15\n\x0b\n\x04\x04\x01\x02\0\
-    \x12\x03\x07\x04\x16\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x07\x04\n\n\
-    \x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x07\x0b\x11\n\x0c\n\x05\x04\x01\x02\
-    \0\x03\x12\x03\x07\x14\x15b\x06proto3\
+    \n\x0fapp_query.proto\"*\n\x0fQueryAppRequest\x12\x17\n\x07app_ids\x18\
+    \x01\x20\x03(\tR\x06appIds\"&\n\rAppIdentifier\x12\x15\n\x06app_id\x18\
+    \x01\x20\x01(\tR\x05appIdJ\xbe\x01\n\x06\x12\x04\0\0\x07\x01\n\x08\n\x01\
+    \x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x04\x01\n\n\n\x03\x04\
+    \0\x01\x12\x03\x02\x08\x17\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x20\n\
+    \x0c\n\x05\x04\0\x02\0\x04\x12\x03\x03\x04\x0c\n\x0c\n\x05\x04\0\x02\0\
+    \x05\x12\x03\x03\r\x13\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x14\x1b\n\
+    \x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x1e\x1f\n\n\n\x02\x04\x01\x12\x04\
+    \x05\0\x07\x01\n\n\n\x03\x04\x01\x01\x12\x03\x05\x08\x15\n\x0b\n\x04\x04\
+    \x01\x02\0\x12\x03\x06\x04\x16\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x06\
+    \x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x06\x0b\x11\n\x0c\n\x05\x04\
+    \x01\x02\0\x03\x12\x03\x06\x14\x15b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 35 - 41
rust-lib/flowy-workspace/src/protobuf/model/observable.rs

@@ -29,9 +29,8 @@ pub enum WorkspaceNotification {
     UserCreateWorkspace = 10,
     UserDeleteWorkspace = 11,
     WorkspaceUpdated = 12,
-    WorkspaceCreateApp = 13,
-    WorkspaceDeleteApp = 14,
-    WorkspaceListUpdated = 15,
+    WorkspaceListUpdated = 13,
+    WorkspaceAppsChanged = 14,
     AppUpdated = 21,
     AppViewsChanged = 24,
     ViewUpdated = 31,
@@ -50,9 +49,8 @@ impl ::protobuf::ProtobufEnum for WorkspaceNotification {
             10 => ::std::option::Option::Some(WorkspaceNotification::UserCreateWorkspace),
             11 => ::std::option::Option::Some(WorkspaceNotification::UserDeleteWorkspace),
             12 => ::std::option::Option::Some(WorkspaceNotification::WorkspaceUpdated),
-            13 => ::std::option::Option::Some(WorkspaceNotification::WorkspaceCreateApp),
-            14 => ::std::option::Option::Some(WorkspaceNotification::WorkspaceDeleteApp),
-            15 => ::std::option::Option::Some(WorkspaceNotification::WorkspaceListUpdated),
+            13 => ::std::option::Option::Some(WorkspaceNotification::WorkspaceListUpdated),
+            14 => ::std::option::Option::Some(WorkspaceNotification::WorkspaceAppsChanged),
             21 => ::std::option::Option::Some(WorkspaceNotification::AppUpdated),
             24 => ::std::option::Option::Some(WorkspaceNotification::AppViewsChanged),
             31 => ::std::option::Option::Some(WorkspaceNotification::ViewUpdated),
@@ -68,9 +66,8 @@ impl ::protobuf::ProtobufEnum for WorkspaceNotification {
             WorkspaceNotification::UserCreateWorkspace,
             WorkspaceNotification::UserDeleteWorkspace,
             WorkspaceNotification::WorkspaceUpdated,
-            WorkspaceNotification::WorkspaceCreateApp,
-            WorkspaceNotification::WorkspaceDeleteApp,
             WorkspaceNotification::WorkspaceListUpdated,
+            WorkspaceNotification::WorkspaceAppsChanged,
             WorkspaceNotification::AppUpdated,
             WorkspaceNotification::AppViewsChanged,
             WorkspaceNotification::ViewUpdated,
@@ -104,40 +101,37 @@ impl ::protobuf::reflect::ProtobufValue for WorkspaceNotification {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x10observable.proto*\x95\x02\n\x15WorkspaceNotification\x12\x0b\n\x07\
+    \n\x10observable.proto*\xff\x01\n\x15WorkspaceNotification\x12\x0b\n\x07\
     Unknown\x10\0\x12\x17\n\x13UserCreateWorkspace\x10\n\x12\x17\n\x13UserDe\
-    leteWorkspace\x10\x0b\x12\x14\n\x10WorkspaceUpdated\x10\x0c\x12\x16\n\
-    \x12WorkspaceCreateApp\x10\r\x12\x16\n\x12WorkspaceDeleteApp\x10\x0e\x12\
-    \x18\n\x14WorkspaceListUpdated\x10\x0f\x12\x0e\n\nAppUpdated\x10\x15\x12\
-    \x13\n\x0fAppViewsChanged\x10\x18\x12\x0f\n\x0bViewUpdated\x10\x1f\x12\
-    \x14\n\x10UserUnauthorized\x10d\x12\x11\n\x0cTrashUpdated\x10\xe8\x07J\
-    \x96\x04\n\x06\x12\x04\0\0\x0f\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\
-    \x02\x05\0\x12\x04\x02\0\x0f\x01\n\n\n\x03\x05\0\x01\x12\x03\x02\x05\x1a\
-    \n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x04\x10\n\x0c\n\x05\x05\0\x02\0\x01\
-    \x12\x03\x03\x04\x0b\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x03\x0e\x0f\n\
-    \x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x04\x1d\n\x0c\n\x05\x05\0\x02\x01\
-    \x01\x12\x03\x04\x04\x17\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\x04\x1a\
-    \x1c\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\x04\x1d\n\x0c\n\x05\x05\0\x02\
-    \x02\x01\x12\x03\x05\x04\x17\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\x05\
-    \x1a\x1c\n\x0b\n\x04\x05\0\x02\x03\x12\x03\x06\x04\x1a\n\x0c\n\x05\x05\0\
-    \x02\x03\x01\x12\x03\x06\x04\x14\n\x0c\n\x05\x05\0\x02\x03\x02\x12\x03\
-    \x06\x17\x19\n\x0b\n\x04\x05\0\x02\x04\x12\x03\x07\x04\x1c\n\x0c\n\x05\
-    \x05\0\x02\x04\x01\x12\x03\x07\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x02\x12\
-    \x03\x07\x19\x1b\n\x0b\n\x04\x05\0\x02\x05\x12\x03\x08\x04\x1c\n\x0c\n\
-    \x05\x05\0\x02\x05\x01\x12\x03\x08\x04\x16\n\x0c\n\x05\x05\0\x02\x05\x02\
-    \x12\x03\x08\x19\x1b\n\x0b\n\x04\x05\0\x02\x06\x12\x03\t\x04\x1e\n\x0c\n\
-    \x05\x05\0\x02\x06\x01\x12\x03\t\x04\x18\n\x0c\n\x05\x05\0\x02\x06\x02\
-    \x12\x03\t\x1b\x1d\n\x0b\n\x04\x05\0\x02\x07\x12\x03\n\x04\x14\n\x0c\n\
-    \x05\x05\0\x02\x07\x01\x12\x03\n\x04\x0e\n\x0c\n\x05\x05\0\x02\x07\x02\
-    \x12\x03\n\x11\x13\n\x0b\n\x04\x05\0\x02\x08\x12\x03\x0b\x04\x19\n\x0c\n\
-    \x05\x05\0\x02\x08\x01\x12\x03\x0b\x04\x13\n\x0c\n\x05\x05\0\x02\x08\x02\
-    \x12\x03\x0b\x16\x18\n\x0b\n\x04\x05\0\x02\t\x12\x03\x0c\x04\x15\n\x0c\n\
-    \x05\x05\0\x02\t\x01\x12\x03\x0c\x04\x0f\n\x0c\n\x05\x05\0\x02\t\x02\x12\
-    \x03\x0c\x12\x14\n\x0b\n\x04\x05\0\x02\n\x12\x03\r\x04\x1b\n\x0c\n\x05\
-    \x05\0\x02\n\x01\x12\x03\r\x04\x14\n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\r\
-    \x17\x1a\n\x0b\n\x04\x05\0\x02\x0b\x12\x03\x0e\x04\x18\n\x0c\n\x05\x05\0\
-    \x02\x0b\x01\x12\x03\x0e\x04\x10\n\x0c\n\x05\x05\0\x02\x0b\x02\x12\x03\
-    \x0e\x13\x17b\x06proto3\
+    leteWorkspace\x10\x0b\x12\x14\n\x10WorkspaceUpdated\x10\x0c\x12\x18\n\
+    \x14WorkspaceListUpdated\x10\r\x12\x18\n\x14WorkspaceAppsChanged\x10\x0e\
+    \x12\x0e\n\nAppUpdated\x10\x15\x12\x13\n\x0fAppViewsChanged\x10\x18\x12\
+    \x0f\n\x0bViewUpdated\x10\x1f\x12\x14\n\x10UserUnauthorized\x10d\x12\x11\
+    \n\x0cTrashUpdated\x10\xe8\x07J\xed\x03\n\x06\x12\x04\0\0\x0e\x01\n\x08\
+    \n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x0e\x01\n\n\n\
+    \x03\x05\0\x01\x12\x03\x02\x05\x1a\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\
+    \x04\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\x0b\n\x0c\n\x05\x05\
+    \0\x02\0\x02\x12\x03\x03\x0e\x0f\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\
+    \x04\x1d\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x04\x17\n\x0c\n\x05\
+    \x05\0\x02\x01\x02\x12\x03\x04\x1a\x1c\n\x0b\n\x04\x05\0\x02\x02\x12\x03\
+    \x05\x04\x1d\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x04\x17\n\x0c\n\
+    \x05\x05\0\x02\x02\x02\x12\x03\x05\x1a\x1c\n\x0b\n\x04\x05\0\x02\x03\x12\
+    \x03\x06\x04\x1a\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x04\x14\n\x0c\
+    \n\x05\x05\0\x02\x03\x02\x12\x03\x06\x17\x19\n\x0b\n\x04\x05\0\x02\x04\
+    \x12\x03\x07\x04\x1e\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x04\x18\n\
+    \x0c\n\x05\x05\0\x02\x04\x02\x12\x03\x07\x1b\x1d\n\x0b\n\x04\x05\0\x02\
+    \x05\x12\x03\x08\x04\x1e\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x08\x04\
+    \x18\n\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x1b\x1d\n\x0b\n\x04\x05\0\
+    \x02\x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\t\x04\
+    \x0e\n\x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x11\x13\n\x0b\n\x04\x05\0\
+    \x02\x07\x12\x03\n\x04\x19\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\n\x04\
+    \x13\n\x0c\n\x05\x05\0\x02\x07\x02\x12\x03\n\x16\x18\n\x0b\n\x04\x05\0\
+    \x02\x08\x12\x03\x0b\x04\x15\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0b\
+    \x04\x0f\n\x0c\n\x05\x05\0\x02\x08\x02\x12\x03\x0b\x12\x14\n\x0b\n\x04\
+    \x05\0\x02\t\x12\x03\x0c\x04\x1b\n\x0c\n\x05\x05\0\x02\t\x01\x12\x03\x0c\
+    \x04\x14\n\x0c\n\x05\x05\0\x02\t\x02\x12\x03\x0c\x17\x1a\n\x0b\n\x04\x05\
+    \0\x02\n\x12\x03\r\x04\x18\n\x0c\n\x05\x05\0\x02\n\x01\x12\x03\r\x04\x10\
+    \n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\r\x13\x17b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 46 - 41
rust-lib/flowy-workspace/src/protobuf/model/trash_create.rs

@@ -886,6 +886,7 @@ impl ::protobuf::reflect::ProtobufValue for RepeatedTrash {
 pub enum TrashType {
     Unknown = 0,
     View = 1,
+    App = 2,
 }
 
 impl ::protobuf::ProtobufEnum for TrashType {
@@ -897,6 +898,7 @@ impl ::protobuf::ProtobufEnum for TrashType {
         match value {
             0 => ::std::option::Option::Some(TrashType::Unknown),
             1 => ::std::option::Option::Some(TrashType::View),
+            2 => ::std::option::Option::Some(TrashType::App),
             _ => ::std::option::Option::None
         }
     }
@@ -905,6 +907,7 @@ impl ::protobuf::ProtobufEnum for TrashType {
         static values: &'static [TrashType] = &[
             TrashType::Unknown,
             TrashType::View,
+            TrashType::App,
         ];
         values
     }
@@ -942,47 +945,49 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     me\x18\x03\x20\x01(\x03R\x0cmodifiedTime\x12\x1f\n\x0bcreate_time\x18\
     \x04\x20\x01(\x03R\ncreateTime\x12\x1a\n\x02ty\x18\x05\x20\x01(\x0e2\n.T\
     rashTypeR\x02ty\"-\n\rRepeatedTrash\x12\x1c\n\x05items\x18\x01\x20\x03(\
-    \x0b2\x06.TrashR\x05items*\"\n\tTrashType\x12\x0b\n\x07Unknown\x10\0\x12\
-    \x08\n\x04View\x10\x01J\x9e\x06\n\x06\x12\x04\0\0\x17\x01\n\x08\n\x01\
-    \x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\n\x03\x04\
-    \0\x01\x12\x03\x02\x08\x18\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04'\n\
-    \x0c\n\x05\x04\0\x02\0\x04\x12\x03\x03\x04\x0c\n\x0c\n\x05\x04\0\x02\0\
-    \x06\x12\x03\x03\r\x1c\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x1d\"\n\
-    \x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03%&\n\x0b\n\x04\x04\0\x02\x01\x12\
-    \x03\x04\x04\x18\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\x04\x08\n\x0c\
-    \n\x05\x04\0\x02\x01\x01\x12\x03\x04\t\x13\n\x0c\n\x05\x04\0\x02\x01\x03\
-    \x12\x03\x04\x16\x17\n\n\n\x02\x04\x01\x12\x04\x06\0\t\x01\n\n\n\x03\x04\
-    \x01\x01\x12\x03\x06\x08\x17\n\x0b\n\x04\x04\x01\x02\0\x12\x03\x07\x04\
-    \x12\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x07\x04\n\n\x0c\n\x05\x04\x01\
-    \x02\0\x01\x12\x03\x07\x0b\r\n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x07\
-    \x10\x11\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\x08\x04\x15\n\x0c\n\x05\x04\
-    \x01\x02\x01\x06\x12\x03\x08\x04\r\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\
-    \x03\x08\x0e\x10\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\x08\x13\x14\n\n\
-    \n\x02\x04\x02\x12\x04\n\0\x10\x01\n\n\n\x03\x04\x02\x01\x12\x03\n\x08\r\
-    \n\x0b\n\x04\x04\x02\x02\0\x12\x03\x0b\x04\x12\n\x0c\n\x05\x04\x02\x02\0\
-    \x05\x12\x03\x0b\x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\x03\x0b\x0b\r\n\
-    \x0c\n\x05\x04\x02\x02\0\x03\x12\x03\x0b\x10\x11\n\x0b\n\x04\x04\x02\x02\
-    \x01\x12\x03\x0c\x04\x14\n\x0c\n\x05\x04\x02\x02\x01\x05\x12\x03\x0c\x04\
-    \n\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\x0c\x0b\x0f\n\x0c\n\x05\x04\
-    \x02\x02\x01\x03\x12\x03\x0c\x12\x13\n\x0b\n\x04\x04\x02\x02\x02\x12\x03\
-    \r\x04\x1c\n\x0c\n\x05\x04\x02\x02\x02\x05\x12\x03\r\x04\t\n\x0c\n\x05\
-    \x04\x02\x02\x02\x01\x12\x03\r\n\x17\n\x0c\n\x05\x04\x02\x02\x02\x03\x12\
-    \x03\r\x1a\x1b\n\x0b\n\x04\x04\x02\x02\x03\x12\x03\x0e\x04\x1a\n\x0c\n\
-    \x05\x04\x02\x02\x03\x05\x12\x03\x0e\x04\t\n\x0c\n\x05\x04\x02\x02\x03\
-    \x01\x12\x03\x0e\n\x15\n\x0c\n\x05\x04\x02\x02\x03\x03\x12\x03\x0e\x18\
-    \x19\n\x0b\n\x04\x04\x02\x02\x04\x12\x03\x0f\x04\x15\n\x0c\n\x05\x04\x02\
-    \x02\x04\x06\x12\x03\x0f\x04\r\n\x0c\n\x05\x04\x02\x02\x04\x01\x12\x03\
-    \x0f\x0e\x10\n\x0c\n\x05\x04\x02\x02\x04\x03\x12\x03\x0f\x13\x14\n\n\n\
-    \x02\x04\x03\x12\x04\x11\0\x13\x01\n\n\n\x03\x04\x03\x01\x12\x03\x11\x08\
-    \x15\n\x0b\n\x04\x04\x03\x02\0\x12\x03\x12\x04\x1d\n\x0c\n\x05\x04\x03\
-    \x02\0\x04\x12\x03\x12\x04\x0c\n\x0c\n\x05\x04\x03\x02\0\x06\x12\x03\x12\
-    \r\x12\n\x0c\n\x05\x04\x03\x02\0\x01\x12\x03\x12\x13\x18\n\x0c\n\x05\x04\
-    \x03\x02\0\x03\x12\x03\x12\x1b\x1c\n\n\n\x02\x05\0\x12\x04\x14\0\x17\x01\
-    \n\n\n\x03\x05\0\x01\x12\x03\x14\x05\x0e\n\x0b\n\x04\x05\0\x02\0\x12\x03\
-    \x15\x04\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x15\x04\x0b\n\x0c\n\x05\
-    \x05\0\x02\0\x02\x12\x03\x15\x0e\x0f\n\x0b\n\x04\x05\0\x02\x01\x12\x03\
-    \x16\x04\r\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x16\x04\x08\n\x0c\n\x05\
-    \x05\0\x02\x01\x02\x12\x03\x16\x0b\x0cb\x06proto3\
+    \x0b2\x06.TrashR\x05items*+\n\tTrashType\x12\x0b\n\x07Unknown\x10\0\x12\
+    \x08\n\x04View\x10\x01\x12\x07\n\x03App\x10\x02J\xc7\x06\n\x06\x12\x04\0\
+    \0\x18\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\
+    \x05\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x18\n\x0b\n\x04\x04\0\x02\0\
+    \x12\x03\x03\x04'\n\x0c\n\x05\x04\0\x02\0\x04\x12\x03\x03\x04\x0c\n\x0c\
+    \n\x05\x04\0\x02\0\x06\x12\x03\x03\r\x1c\n\x0c\n\x05\x04\0\x02\0\x01\x12\
+    \x03\x03\x1d\"\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03%&\n\x0b\n\x04\x04\
+    \0\x02\x01\x12\x03\x04\x04\x18\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\
+    \x04\x08\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\t\x13\n\x0c\n\x05\x04\
+    \0\x02\x01\x03\x12\x03\x04\x16\x17\n\n\n\x02\x04\x01\x12\x04\x06\0\t\x01\
+    \n\n\n\x03\x04\x01\x01\x12\x03\x06\x08\x17\n\x0b\n\x04\x04\x01\x02\0\x12\
+    \x03\x07\x04\x12\n\x0c\n\x05\x04\x01\x02\0\x05\x12\x03\x07\x04\n\n\x0c\n\
+    \x05\x04\x01\x02\0\x01\x12\x03\x07\x0b\r\n\x0c\n\x05\x04\x01\x02\0\x03\
+    \x12\x03\x07\x10\x11\n\x0b\n\x04\x04\x01\x02\x01\x12\x03\x08\x04\x15\n\
+    \x0c\n\x05\x04\x01\x02\x01\x06\x12\x03\x08\x04\r\n\x0c\n\x05\x04\x01\x02\
+    \x01\x01\x12\x03\x08\x0e\x10\n\x0c\n\x05\x04\x01\x02\x01\x03\x12\x03\x08\
+    \x13\x14\n\n\n\x02\x04\x02\x12\x04\n\0\x10\x01\n\n\n\x03\x04\x02\x01\x12\
+    \x03\n\x08\r\n\x0b\n\x04\x04\x02\x02\0\x12\x03\x0b\x04\x12\n\x0c\n\x05\
+    \x04\x02\x02\0\x05\x12\x03\x0b\x04\n\n\x0c\n\x05\x04\x02\x02\0\x01\x12\
+    \x03\x0b\x0b\r\n\x0c\n\x05\x04\x02\x02\0\x03\x12\x03\x0b\x10\x11\n\x0b\n\
+    \x04\x04\x02\x02\x01\x12\x03\x0c\x04\x14\n\x0c\n\x05\x04\x02\x02\x01\x05\
+    \x12\x03\x0c\x04\n\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\x0c\x0b\x0f\n\
+    \x0c\n\x05\x04\x02\x02\x01\x03\x12\x03\x0c\x12\x13\n\x0b\n\x04\x04\x02\
+    \x02\x02\x12\x03\r\x04\x1c\n\x0c\n\x05\x04\x02\x02\x02\x05\x12\x03\r\x04\
+    \t\n\x0c\n\x05\x04\x02\x02\x02\x01\x12\x03\r\n\x17\n\x0c\n\x05\x04\x02\
+    \x02\x02\x03\x12\x03\r\x1a\x1b\n\x0b\n\x04\x04\x02\x02\x03\x12\x03\x0e\
+    \x04\x1a\n\x0c\n\x05\x04\x02\x02\x03\x05\x12\x03\x0e\x04\t\n\x0c\n\x05\
+    \x04\x02\x02\x03\x01\x12\x03\x0e\n\x15\n\x0c\n\x05\x04\x02\x02\x03\x03\
+    \x12\x03\x0e\x18\x19\n\x0b\n\x04\x04\x02\x02\x04\x12\x03\x0f\x04\x15\n\
+    \x0c\n\x05\x04\x02\x02\x04\x06\x12\x03\x0f\x04\r\n\x0c\n\x05\x04\x02\x02\
+    \x04\x01\x12\x03\x0f\x0e\x10\n\x0c\n\x05\x04\x02\x02\x04\x03\x12\x03\x0f\
+    \x13\x14\n\n\n\x02\x04\x03\x12\x04\x11\0\x13\x01\n\n\n\x03\x04\x03\x01\
+    \x12\x03\x11\x08\x15\n\x0b\n\x04\x04\x03\x02\0\x12\x03\x12\x04\x1d\n\x0c\
+    \n\x05\x04\x03\x02\0\x04\x12\x03\x12\x04\x0c\n\x0c\n\x05\x04\x03\x02\0\
+    \x06\x12\x03\x12\r\x12\n\x0c\n\x05\x04\x03\x02\0\x01\x12\x03\x12\x13\x18\
+    \n\x0c\n\x05\x04\x03\x02\0\x03\x12\x03\x12\x1b\x1c\n\n\n\x02\x05\0\x12\
+    \x04\x14\0\x18\x01\n\n\n\x03\x05\0\x01\x12\x03\x14\x05\x0e\n\x0b\n\x04\
+    \x05\0\x02\0\x12\x03\x15\x04\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x15\
+    \x04\x0b\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x15\x0e\x0f\n\x0b\n\x04\x05\
+    \0\x02\x01\x12\x03\x16\x04\r\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x16\
+    \x04\x08\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\x16\x0b\x0c\n\x0b\n\x04\
+    \x05\0\x02\x02\x12\x03\x17\x04\x0c\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\
+    \x17\x04\x07\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\x17\n\x0bb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 2
rust-lib/flowy-workspace/src/protobuf/proto/app_query.proto

@@ -1,8 +1,7 @@
 syntax = "proto3";
 
 message QueryAppRequest {
-    string app_id = 1;
-    bool is_trash = 2;
+    repeated string app_ids = 1;
 }
 message AppIdentifier {
     string app_id = 1;

+ 2 - 3
rust-lib/flowy-workspace/src/protobuf/proto/observable.proto

@@ -5,9 +5,8 @@ enum WorkspaceNotification {
     UserCreateWorkspace = 10;
     UserDeleteWorkspace = 11;
     WorkspaceUpdated = 12;
-    WorkspaceCreateApp = 13;
-    WorkspaceDeleteApp = 14;
-    WorkspaceListUpdated = 15;
+    WorkspaceListUpdated = 13;
+    WorkspaceAppsChanged = 14;
     AppUpdated = 21;
     AppViewsChanged = 24;
     ViewUpdated = 31;

+ 1 - 0
rust-lib/flowy-workspace/src/protobuf/proto/trash_create.proto

@@ -21,4 +21,5 @@ message RepeatedTrash {
 enum TrashType {
     Unknown = 0;
     View = 1;
+    App = 2;
 }

+ 180 - 65
rust-lib/flowy-workspace/src/services/app_controller.rs

@@ -1,25 +1,43 @@
 use crate::{
-    entities::app::{App, CreateAppParams, *},
+    entities::{
+        app::{App, CreateAppParams, *},
+        trash::TrashType,
+    },
     errors::*,
     module::{WorkspaceDatabase, WorkspaceUser},
     notify::*,
-    services::{helper::spawn, server::Server},
+    services::{helper::spawn, server::Server, TrashCan, TrashEvent},
     sql_tables::app::{AppTable, AppTableChangeset, AppTableSql},
 };
-
 use flowy_database::SqliteConnection;
-
-use std::sync::Arc;
+use futures::{FutureExt, StreamExt};
+use std::{collections::HashSet, sync::Arc};
 
 pub(crate) struct AppController {
     user: Arc<dyn WorkspaceUser>,
     database: Arc<dyn WorkspaceDatabase>,
+    trash_can: Arc<TrashCan>,
     server: Server,
 }
 
 impl AppController {
-    pub(crate) fn new(user: Arc<dyn WorkspaceUser>, database: Arc<dyn WorkspaceDatabase>, server: Server) -> Self {
-        Self { user, database, server }
+    pub(crate) fn new(
+        user: Arc<dyn WorkspaceUser>,
+        database: Arc<dyn WorkspaceDatabase>,
+        trash_can: Arc<TrashCan>,
+        server: Server,
+    ) -> Self {
+        Self {
+            user,
+            database,
+            trash_can,
+            server,
+        }
+    }
+
+    pub fn init(&self) -> Result<(), WorkspaceError> {
+        self.listen_trash_can_event();
+        Ok(())
     }
 
     #[tracing::instrument(level = "debug", skip(self), err)]
@@ -29,10 +47,7 @@ impl AppController {
 
         conn.immediate_transaction::<_, WorkspaceError, _>(|| {
             let _ = self.save_app(app.clone(), &*conn)?;
-            let apps = self.read_local_apps(&app.workspace_id, &*conn)?;
-            send_dart_notification(&app.workspace_id, WorkspaceNotification::WorkspaceCreateApp)
-                .payload(apps)
-                .send();
+            let _ = notify_app_num_changed(&app.workspace_id, self.trash_can.clone(), conn)?;
             Ok(())
         })?;
 
@@ -46,31 +61,16 @@ impl AppController {
     }
 
     pub(crate) async fn read_app(&self, params: AppIdentifier) -> Result<App, WorkspaceError> {
-        let app_table = AppTableSql::read_app(&params.app_id, &*self.database.db_connection()?)?;
-        let _ = self.read_app_on_server(params)?;
-        Ok(app_table.into())
-    }
-
-    #[tracing::instrument(level = "debug", skip(self), err)]
-    pub(crate) async fn delete_app(&self, app_id: &str) -> Result<(), WorkspaceError> {
-        let conn = &*self.database.db_connection()?;
-        conn.immediate_transaction::<_, WorkspaceError, _>(|| {
-            let app = AppTableSql::delete_app(app_id, &*conn)?;
-            let apps = self.read_local_apps(&app.workspace_id, &*conn)?;
-            send_dart_notification(&app.workspace_id, WorkspaceNotification::WorkspaceDeleteApp)
-                .payload(apps)
-                .send();
-            Ok(())
-        })?;
+        let conn = self.database.db_connection()?;
+        let app_table = AppTableSql::read_app(&params.app_id, &*conn)?;
 
-        let _ = self.delete_app_on_server(app_id);
-        Ok(())
-    }
+        let trash_ids = self.trash_can.trash_ids(&conn)?;
+        if trash_ids.contains(&app_table.id) {
+            return Err(WorkspaceError::record_not_found());
+        }
 
-    fn read_local_apps(&self, workspace_id: &str, conn: &SqliteConnection) -> Result<RepeatedApp, WorkspaceError> {
-        let app_tables = AppTableSql::read_apps(workspace_id, false, conn)?;
-        let apps = app_tables.into_iter().map(|table| table.into()).collect::<Vec<App>>();
-        Ok(RepeatedApp { items: apps })
+        let _ = self.read_app_on_server(params)?;
+        Ok(app_table.into())
     }
 
     pub(crate) async fn update_app(&self, params: UpdateAppParams) -> Result<(), WorkspaceError> {
@@ -89,6 +89,19 @@ impl AppController {
         let _ = self.update_app_on_server(params)?;
         Ok(())
     }
+
+    pub(crate) fn read_app_tables(&self, ids: Vec<String>) -> Result<Vec<AppTable>, WorkspaceError> {
+        let conn = &*self.database.db_connection()?;
+        let mut app_tables = vec![];
+        conn.immediate_transaction::<_, WorkspaceError, _>(|| {
+            for app_id in ids {
+                app_tables.push(AppTableSql::read_app(&app_id, conn)?);
+            }
+            Ok(())
+        })?;
+
+        Ok(app_tables)
+    }
 }
 
 impl AppController {
@@ -115,37 +128,6 @@ impl AppController {
         Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip(self), err)]
-    fn delete_app_on_server(&self, app_id: &str) -> Result<(), WorkspaceError> {
-        let token = self.user.token()?;
-        let server = self.server.clone();
-        let params = DeleteAppParams {
-            app_id: app_id.to_string(),
-        };
-        spawn(async move {
-            match server.delete_app(&token, params).await {
-                Ok(_) => {},
-                Err(e) => {
-                    // TODO: retry?
-                    log::error!("Delete app failed: {:?}", e);
-                },
-            }
-        });
-        // let action = RetryAction::new(self.server.clone(), self.user.clone(), move
-        // |token, server| {     let params = params.clone();
-        //     async move {
-        //         match server.delete_app(&token, params).await {
-        //             Ok(_) => {},
-        //             Err(e) => log::error!("Delete app failed: {:?}", e),
-        //         }
-        //         Ok::<(), WorkspaceError>(())
-        //     }
-        // });
-        //
-        // spawn_retry(500, 3, action);
-        Ok(())
-    }
-
     #[tracing::instrument(level = "debug", skip(self), err)]
     fn read_app_on_server(&self, params: AppIdentifier) -> Result<(), WorkspaceError> {
         let token = self.user.token()?;
@@ -175,4 +157,137 @@ impl AppController {
         });
         Ok(())
     }
+
+    fn listen_trash_can_event(&self) {
+        let mut rx = self.trash_can.subscribe();
+        let database = self.database.clone();
+        let trash_can = self.trash_can.clone();
+        let _ = tokio::spawn(async move {
+            loop {
+                let mut stream = Box::pin(rx.recv().into_stream().filter_map(|result| async move {
+                    match result {
+                        Ok(event) => event.select(TrashType::App),
+                        Err(_e) => None,
+                    }
+                }));
+                match stream.next().await {
+                    Some(event) => handle_trash_event(database.clone(), trash_can.clone(), event).await,
+                    None => {},
+                }
+            }
+        });
+    }
+}
+
+async fn handle_trash_event(database: Arc<dyn WorkspaceDatabase>, trash_can: Arc<TrashCan>, event: TrashEvent) {
+    let db_result = database.db_connection();
+    match event {
+        TrashEvent::NewTrash(identifiers, ret) | TrashEvent::Putback(identifiers, ret) => {
+            let result = || {
+                let conn = &*db_result?;
+                let _ = conn.immediate_transaction::<_, WorkspaceError, _>(|| {
+                    for identifier in identifiers.items {
+                        let app_table = AppTableSql::read_app(&identifier.id, conn)?;
+                        let _ = notify_app_num_changed(&app_table.workspace_id, trash_can.clone(), conn)?;
+                    }
+                    Ok(())
+                })?;
+                Ok::<(), WorkspaceError>(())
+            };
+            let _ = ret.send(result()).await;
+        },
+        TrashEvent::Delete(identifiers, ret) => {
+            let result = || {
+                let conn = &*db_result?;
+                let _ = conn.immediate_transaction::<_, WorkspaceError, _>(|| {
+                    let mut notify_ids = HashSet::new();
+                    for identifier in identifiers.items {
+                        let app_table = AppTableSql::read_app(&identifier.id, conn)?;
+                        let _ = AppTableSql::delete_app(&identifier.id, conn)?;
+                        notify_ids.insert(app_table.workspace_id);
+                    }
+
+                    for notify_id in notify_ids {
+                        let _ = notify_app_num_changed(&notify_id, trash_can.clone(), conn)?;
+                    }
+                    Ok(())
+                })?;
+                Ok::<(), WorkspaceError>(())
+            };
+            let _ = ret.send(result()).await;
+        },
+    }
 }
+
+#[tracing::instrument(skip(workspace_id, trash_can, conn), err)]
+fn notify_app_num_changed(
+    workspace_id: &str,
+    trash_can: Arc<TrashCan>,
+    conn: &SqliteConnection,
+) -> WorkspaceResult<()> {
+    let repeated_app = read_local_workspace_apps(workspace_id, trash_can, conn)?;
+    send_dart_notification(workspace_id, WorkspaceNotification::WorkspaceAppsChanged)
+        .payload(repeated_app)
+        .send();
+    Ok(())
+}
+
+fn read_local_workspace_apps(
+    workspace_id: &str,
+    trash_can: Arc<TrashCan>,
+    conn: &SqliteConnection,
+) -> Result<RepeatedApp, WorkspaceError> {
+    let mut app_tables = AppTableSql::read_workspace_apps(workspace_id, false, conn)?;
+    let trash_ids = trash_can.trash_ids(conn)?;
+    app_tables.retain(|app_table| !trash_ids.contains(&app_table.id));
+
+    let apps = app_tables.into_iter().map(|table| table.into()).collect::<Vec<App>>();
+    Ok(RepeatedApp { items: apps })
+}
+
+// #[tracing::instrument(level = "debug", skip(self), err)]
+// pub(crate) async fn delete_app(&self, app_id: &str) -> Result<(),
+// WorkspaceError> {     let conn = &*self.database.db_connection()?;
+//     conn.immediate_transaction::<_, WorkspaceError, _>(|| {
+//         let app = AppTableSql::delete_app(app_id, &*conn)?;
+//         let apps = self.read_local_apps(&app.workspace_id, &*conn)?;
+//         send_dart_notification(&app.workspace_id,
+// WorkspaceNotification::WorkspaceDeleteApp)             .payload(apps)
+//             .send();
+//         Ok(())
+//     })?;
+//
+//     let _ = self.delete_app_on_server(app_id);
+//     Ok(())
+// }
+//
+// #[tracing::instrument(level = "debug", skip(self), err)]
+// fn delete_app_on_server(&self, app_id: &str) -> Result<(), WorkspaceError> {
+//     let token = self.user.token()?;
+//     let server = self.server.clone();
+//     let params = DeleteAppParams {
+//         app_id: app_id.to_string(),
+//     };
+//     spawn(async move {
+//         match server.delete_app(&token, params).await {
+//             Ok(_) => {},
+//             Err(e) => {
+//                 // TODO: retry?
+//                 log::error!("Delete app failed: {:?}", e);
+//             },
+//         }
+//     });
+//     // let action = RetryAction::new(self.server.clone(), self.user.clone(),
+// move     // |token, server| {     let params = params.clone();
+//     //     async move {
+//     //         match server.delete_app(&token, params).await {
+//     //             Ok(_) => {},
+//     //             Err(e) => log::error!("Delete app failed: {:?}", e),
+//     //         }
+//     //         Ok::<(), WorkspaceError>(())
+//     //     }
+//     // });
+//     //
+//     // spawn_retry(500, 3, action);
+//     Ok(())
+// }

+ 2 - 2
rust-lib/flowy-workspace/src/services/server/mod.rs

@@ -8,7 +8,7 @@ pub use server_api_mock::*;
 
 use crate::{
     entities::{
-        app::{App, AppIdentifier, CreateAppParams, DeleteAppParams, UpdateAppParams},
+        app::{App, AppIdentifier, CreateAppParams, UpdateAppParams},
         trash::{RepeatedTrash, TrashIdentifiers},
         view::{CreateViewParams, UpdateViewParams, View, ViewIdentifier, ViewIdentifiers},
         workspace::{
@@ -58,7 +58,7 @@ pub trait WorkspaceServerAPI {
 
     fn update_app(&self, token: &str, params: UpdateAppParams) -> ResultFuture<(), WorkspaceError>;
 
-    fn delete_app(&self, token: &str, params: DeleteAppParams) -> ResultFuture<(), WorkspaceError>;
+    fn delete_app(&self, token: &str, params: AppIdentifier) -> ResultFuture<(), WorkspaceError>;
 
     // Trash
     fn create_trash(&self, token: &str, params: TrashIdentifiers) -> ResultFuture<(), WorkspaceError>;

+ 3 - 3
rust-lib/flowy-workspace/src/services/server/server_api.rs

@@ -1,6 +1,6 @@
 use crate::{
     entities::{
-        app::{App, AppIdentifier, CreateAppParams, DeleteAppParams, UpdateAppParams},
+        app::{App, AppIdentifier, CreateAppParams, UpdateAppParams},
         trash::{RepeatedTrash, TrashIdentifiers},
         view::{CreateViewParams, UpdateViewParams, View, ViewIdentifier, ViewIdentifiers},
         workspace::{
@@ -97,7 +97,7 @@ impl WorkspaceServerAPI for WorkspaceServer {
         ResultFuture::new(async move { update_app_request(&token, params, &url).await })
     }
 
-    fn delete_app(&self, token: &str, params: DeleteAppParams) -> ResultFuture<(), WorkspaceError> {
+    fn delete_app(&self, token: &str, params: AppIdentifier) -> ResultFuture<(), WorkspaceError> {
         let token = token.to_owned();
         let url = self.config.app_url();
         ResultFuture::new(async move { delete_app_request(&token, params, &url).await })
@@ -214,7 +214,7 @@ pub async fn update_app_request(token: &str, params: UpdateAppParams, url: &str)
     Ok(())
 }
 
-pub async fn delete_app_request(token: &str, params: DeleteAppParams, url: &str) -> Result<(), WorkspaceError> {
+pub async fn delete_app_request(token: &str, params: AppIdentifier, url: &str) -> Result<(), WorkspaceError> {
     let _ = request_builder()
         .delete(&url.to_owned())
         .header(HEADER_TOKEN, token)

+ 2 - 2
rust-lib/flowy-workspace/src/services/server/server_api_mock.rs

@@ -1,6 +1,6 @@
 use crate::{
     entities::{
-        app::{App, AppIdentifier, CreateAppParams, DeleteAppParams, RepeatedApp, UpdateAppParams},
+        app::{App, AppIdentifier, CreateAppParams, RepeatedApp, UpdateAppParams},
         trash::{RepeatedTrash, TrashIdentifiers},
         view::{CreateViewParams, RepeatedView, UpdateViewParams, View, ViewIdentifier, ViewIdentifiers},
         workspace::{
@@ -104,7 +104,7 @@ impl WorkspaceServerAPI for WorkspaceServerMock {
         ResultFuture::new(async { Ok(()) })
     }
 
-    fn delete_app(&self, _token: &str, _params: DeleteAppParams) -> ResultFuture<(), WorkspaceError> {
+    fn delete_app(&self, _token: &str, _params: AppIdentifier) -> ResultFuture<(), WorkspaceError> {
         ResultFuture::new(async { Ok(()) })
     }
 

+ 14 - 10
rust-lib/flowy-workspace/src/services/view_controller.rs

@@ -64,10 +64,8 @@ impl ViewController {
 
         conn.immediate_transaction::<_, WorkspaceError, _>(|| {
             let _ = self.save_view(view.clone(), conn)?;
-            let repeated_view = read_belonging_view(&view.belong_to_id, trash_can, &conn)?;
-            send_dart_notification(&view.belong_to_id, WorkspaceNotification::AppViewsChanged)
-                .payload(repeated_view)
-                .send();
+            let _ = notify_view_num_changed(&view.belong_to_id, trash_can, &conn)?;
+
             Ok(())
         })?;
 
@@ -145,7 +143,7 @@ impl ViewController {
     pub(crate) async fn read_views_belong_to(&self, belong_to_id: &str) -> Result<RepeatedView, WorkspaceError> {
         // TODO: read from server
         let conn = self.database.db_connection()?;
-        let repeated_view = read_belonging_view(belong_to_id, self.trash_can.clone(), &conn)?;
+        let repeated_view = read_local_belonging_view(belong_to_id, self.trash_can.clone(), &conn)?;
         Ok(repeated_view)
     }
 
@@ -309,7 +307,7 @@ fn notify_view_num_changed(
     trash_can: Arc<TrashCan>,
     conn: &SqliteConnection,
 ) -> WorkspaceResult<()> {
-    let repeated_view = read_belonging_view(belong_to_id, trash_can.clone(), conn)?;
+    let repeated_view = read_local_belonging_view(belong_to_id, trash_can.clone(), conn)?;
     tracing::Span::current().record("view_count", &format!("{}", repeated_view.len()).as_str());
     send_dart_notification(&belong_to_id, WorkspaceNotification::AppViewsChanged)
         .payload(repeated_view)
@@ -317,13 +315,19 @@ fn notify_view_num_changed(
     Ok(())
 }
 
-fn read_belonging_view(
+fn read_local_belonging_view(
     belong_to_id: &str,
     trash_can: Arc<TrashCan>,
     conn: &SqliteConnection,
 ) -> WorkspaceResult<RepeatedView> {
-    let mut repeated_view = ViewTableSql::read_views(belong_to_id, conn)?;
+    let mut view_tables = ViewTableSql::read_views(belong_to_id, conn)?;
     let trash_ids = trash_can.trash_ids(conn)?;
-    repeated_view.retain(|view| !trash_ids.contains(&view.id));
-    Ok(repeated_view)
+    view_tables.retain(|view_table| !trash_ids.contains(&view_table.id));
+
+    let views = view_tables
+        .into_iter()
+        .map(|view_table| view_table.into())
+        .collect::<Vec<View>>();
+
+    Ok(RepeatedView { items: views })
 }

+ 1 - 0
rust-lib/flowy-workspace/src/services/workspace_controller.rs

@@ -47,6 +47,7 @@ impl WorkspaceController {
     pub fn init(&self) -> Result<(), WorkspaceError> {
         let _ = self.trash_can.init()?;
         let _ = self.view_controller.init()?;
+        let _ = self.app_controller.init()?;
         Ok(())
     }
 

+ 1 - 6
rust-lib/flowy-workspace/src/sql_tables/app/app_sql.rs

@@ -29,16 +29,11 @@ impl AppTableSql {
 
     pub(crate) fn read_app(app_id: &str, conn: &SqliteConnection) -> Result<AppTable, WorkspaceError> {
         let filter = dsl::app_table.filter(app_table::id.eq(app_id)).into_boxed();
-
-        // if let Some(is_trash) = is_trash {
-        //     filter = filter.filter(app_table::is_trash.eq(is_trash));
-        // }
-
         let app_table = filter.first::<AppTable>(conn)?;
         Ok(app_table)
     }
 
-    pub(crate) fn read_apps(
+    pub(crate) fn read_workspace_apps(
         workspace_id: &str,
         is_trash: bool,
         conn: &SqliteConnection,

+ 13 - 0
rust-lib/flowy-workspace/src/sql_tables/app/app_table.rs

@@ -8,6 +8,7 @@ use crate::{
 use diesel::sql_types::Binary;
 use flowy_database::schema::app_table;
 
+use crate::entities::trash::{Trash, TrashType};
 use serde::{Deserialize, Serialize, __private::TryFrom};
 use std::convert::TryInto;
 
@@ -44,6 +45,18 @@ impl AppTable {
     }
 }
 
+impl std::convert::Into<Trash> for AppTable {
+    fn into(self) -> Trash {
+        Trash {
+            id: self.id,
+            name: self.name,
+            modified_time: self.modified_time,
+            create_time: self.create_time,
+            ty: TrashType::App,
+        }
+    }
+}
+
 #[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Default, FromSqlRow, AsExpression)]
 #[sql_type = "Binary"]
 pub(crate) struct ColorStyleCol {

+ 4 - 0
rust-lib/flowy-workspace/src/sql_tables/trash/trash_table.rs

@@ -61,6 +61,7 @@ impl std::convert::From<TrashTable> for TrashTableChangeset {
 pub(crate) enum SqlTrashType {
     Unknown = 0,
     View    = 1,
+    App     = 2,
 }
 
 impl std::convert::From<i32> for SqlTrashType {
@@ -68,6 +69,7 @@ impl std::convert::From<i32> for SqlTrashType {
         match value {
             0 => SqlTrashType::Unknown,
             1 => SqlTrashType::View,
+            2 => SqlTrashType::App,
             _o => SqlTrashType::Unknown,
         }
     }
@@ -80,6 +82,7 @@ impl std::convert::Into<TrashType> for SqlTrashType {
         match self {
             SqlTrashType::Unknown => TrashType::Unknown,
             SqlTrashType::View => TrashType::View,
+            SqlTrashType::App => TrashType::App,
         }
     }
 }
@@ -89,6 +92,7 @@ impl std::convert::From<TrashType> for SqlTrashType {
         match ty {
             TrashType::Unknown => SqlTrashType::Unknown,
             TrashType::View => SqlTrashType::View,
+            TrashType::App => SqlTrashType::App,
         }
     }
 }

+ 2 - 8
rust-lib/flowy-workspace/src/sql_tables/view/view_sql.rs

@@ -1,5 +1,4 @@
 use crate::{
-    entities::view::{RepeatedView, View},
     errors::WorkspaceError,
     sql_tables::view::{ViewTable, ViewTableChangeset},
 };
@@ -39,18 +38,13 @@ impl ViewTableSql {
     }
 
     // belong_to_id will be the app_id or view_id.
-    pub(crate) fn read_views(belong_to_id: &str, conn: &SqliteConnection) -> Result<RepeatedView, WorkspaceError> {
+    pub(crate) fn read_views(belong_to_id: &str, conn: &SqliteConnection) -> Result<Vec<ViewTable>, WorkspaceError> {
         let view_tables = dsl::view_table
             .filter(view_table::belong_to_id.eq(belong_to_id))
             .into_boxed()
             .load::<ViewTable>(conn)?;
 
-        let views = view_tables
-            .into_iter()
-            .map(|view_table| view_table.into())
-            .collect::<Vec<View>>();
-
-        Ok(RepeatedView { items: views })
+        Ok(view_tables)
     }
 
     pub(crate) fn update_view(changeset: ViewTableChangeset, conn: &SqliteConnection) -> Result<(), WorkspaceError> {

+ 9 - 3
rust-lib/flowy-workspace/tests/workspace/app_test.rs

@@ -6,14 +6,18 @@ use flowy_workspace::entities::{app::QueryAppRequest, view::*};
 async fn app_delete() {
     let test = AppTest::new().await;
     delete_app(&test.sdk, &test.app.id).await;
-    let query = QueryAppRequest::new(&test.app.id);
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
     let _ = read_app(&test.sdk, query).await;
 }
 
 #[tokio::test]
 async fn app_read() {
     let test = AppTest::new().await;
-    let query = QueryAppRequest::new(&test.app.id);
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
     let app_from_db = read_app(&test.sdk, query).await;
     assert_eq!(app_from_db, test.app);
 }
@@ -40,7 +44,9 @@ async fn app_create_with_view() {
     let view_a = create_view_with_request(&test.sdk, request_a).await;
     let view_b = create_view_with_request(&test.sdk, request_b).await;
 
-    let query = QueryAppRequest::new(&test.app.id);
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
     let view_from_db = read_app(&test.sdk, query).await;
 
     assert_eq!(view_from_db.belongings[0], view_a);

+ 10 - 6
rust-lib/flowy-workspace/tests/workspace/view_test.rs

@@ -9,7 +9,7 @@ use flowy_workspace::entities::{
 #[should_panic]
 async fn view_delete() {
     let test = FlowyTest::setup();
-    let _ = test.init_user();
+    let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
     test.delete_views(vec![test.view.id.clone()]).await;
@@ -22,7 +22,7 @@ async fn view_delete() {
 #[tokio::test]
 async fn view_delete_then_putback() {
     let test = FlowyTest::setup();
-    let _ = test.init_user();
+    let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
     test.delete_views(vec![test.view.id.clone()]).await;
@@ -45,7 +45,7 @@ async fn view_delete_then_putback() {
 #[tokio::test]
 async fn view_delete_all() {
     let test = FlowyTest::setup();
-    let _ = test.init_user();
+    let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
     let view1 = test.view.clone();
@@ -53,7 +53,9 @@ async fn view_delete_all() {
     let view3 = create_view(&test.sdk, &test.app.id).await;
     let view_ids = vec![view1.id.clone(), view2.id.clone(), view3.id.clone()];
 
-    let query = QueryAppRequest::new(&test.app.id);
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
     let app = read_app(&test.sdk, query.clone()).await;
     assert_eq!(app.belongings.len(), view_ids.len());
     test.delete_views(view_ids.clone()).await;
@@ -65,7 +67,7 @@ async fn view_delete_all() {
 #[tokio::test]
 async fn view_delete_all_permanent() {
     let test = FlowyTest::setup();
-    let _ = test.init_user();
+    let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
     let view1 = test.view.clone();
@@ -74,7 +76,9 @@ async fn view_delete_all_permanent() {
     let view_ids = vec![view1.id.clone(), view2.id.clone()];
     test.delete_views_permanent(view_ids).await;
 
-    let query = QueryAppRequest::new(&test.app.id);
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
     assert_eq!(read_app(&test.sdk, query).await.belongings.len(), 0);
     assert_eq!(read_trash(&test.sdk).await.len(), 0);
 }