diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index 09f49ab869..e9989414b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -38,4 +38,7 @@ class LoadingState with _$LoadingState { const factory LoadingState.finish( Either successOrFail, ) = _Finish; + + const LoadingState._(); + isLoading() => this is _Loading; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart index 2d6aced74e..0e64ba597c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_document_bloc.dart @@ -62,7 +62,7 @@ class RowDocumentBloc extends Bloc { viewsOrError.fold( (view) => add(RowDocumentEvent.didReceiveRowDocument(view)), (error) async { - if (error.code == ErrorCode.RecordNotFound.value) { + if (error.code == ErrorCode.RecordNotFound) { // By default, the document of the row is not exist. So creating a // new document for the given document id of the row. final documentView = diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 79bc7f7f49..12c88b4a23 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -107,7 +107,7 @@ class DateCellCalendarBloc } }, (err) { - switch (ErrorCode.valueOf(err.code)!) { + switch (err.code) { case ErrorCode.InvalidDateTimeFormat: if (isClosed) return; add( diff --git a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart index 40fefc6da2..7ac6e5cd2e 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -81,7 +82,7 @@ class AppFlowyAuthService implements AuthService { }) async { return left( FlowyError.create() - ..code = 0 + ..code = ErrorCode.Internal ..msg = "Unsupported sign up action", ); } @@ -98,7 +99,7 @@ class AppFlowyAuthService implements AuthService { }) async { return left( FlowyError.create() - ..code = 0 + ..code = ErrorCode.Internal ..msg = "Unsupported sign up action", ); } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart index 859f187b46..0f730c6974 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart @@ -1,19 +1,20 @@ +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; class AuthError { static final supabaseSignInError = FlowyError() - ..msg = 'supabase sign in error' - ..code = -10001; + ..msg = 'supabase sign in error -10001' + ..code = ErrorCode.UserUnauthorized; static final supabaseSignUpError = FlowyError() - ..msg = 'supabase sign up error' - ..code = -10002; + ..msg = 'supabase sign up error -10002' + ..code = ErrorCode.UserUnauthorized; static final supabaseSignInWithOauthError = FlowyError() - ..msg = 'supabase sign in with oauth error' - ..code = -10003; + ..msg = 'supabase sign in with oauth error -10003' + ..code = ErrorCode.UserUnauthorized; static final supabaseGetUserError = FlowyError() - ..msg = 'unable to get user from supabase' - ..code = -10004; + ..msg = 'unable to get user from supabase -10004' + ..code = ErrorCode.UserUnauthorized; } diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index e9cb15a483..749e12fe4b 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -156,7 +156,7 @@ class SignInBloc extends Bloc { } SignInState stateFromCode(FlowyError error) { - switch (ErrorCode.valueOf(error.code)) { + switch (error.code) { case ErrorCode.EmailFormatInvalid: return state.copyWith( isSubmitting: false, diff --git a/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart index ca884cdecc..fe796960b3 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart @@ -119,7 +119,7 @@ class SignUpBloc extends Bloc { } SignUpState stateFromCode(FlowyError error) { - switch (ErrorCode.valueOf(error.code)!) { + switch (error.code) { case ErrorCode.EmailFormatInvalid: return state.copyWith( isSubmitting: false, diff --git a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart new file mode 100644 index 0000000000..91b9e2e5fc --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart @@ -0,0 +1,98 @@ +import 'package:appflowy/plugins/database_view/application/defines.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'workspace_error_bloc.freezed.dart'; + +class WorkspaceErrorBloc + extends Bloc { + final UserFolderPB userFolder; + + WorkspaceErrorBloc({ + required this.userFolder, + required FlowyError error, + }) : super(WorkspaceErrorState.initial(error)) { + on((event, emit) async { + await event.when( + init: () { + // _loadSnapshots(); + }, + resetWorkspace: () async { + emit(state.copyWith(loadingState: const LoadingState.loading())); + final payload = ResetWorkspacePB.create() + ..workspaceId = userFolder.workspaceId + ..uid = userFolder.uid; + UserEventResetWorkspace(payload).send().then( + (result) { + if (isClosed) { + return; + } + add(WorkspaceErrorEvent.didResetWorkspace(result)); + }, + ); + }, + didResetWorkspace: (result) { + result.fold( + (_) { + emit( + state.copyWith( + loadingState: LoadingState.finish(result), + workspaceState: const WorkspaceState.reset(), + ), + ); + }, + (err) { + emit(state.copyWith(loadingState: LoadingState.finish(result))); + }, + ); + }, + logout: () { + emit( + state.copyWith( + workspaceState: const WorkspaceState.logout(), + ), + ); + }, + ); + }); + } +} + +@freezed +class WorkspaceErrorEvent with _$WorkspaceErrorEvent { + const factory WorkspaceErrorEvent.init() = _Init; + const factory WorkspaceErrorEvent.logout() = _DidLogout; + const factory WorkspaceErrorEvent.resetWorkspace() = _ResetWorkspace; + const factory WorkspaceErrorEvent.didResetWorkspace( + Either result, + ) = _DidResetWorkspace; +} + +@freezed +class WorkspaceErrorState with _$WorkspaceErrorState { + const factory WorkspaceErrorState({ + required FlowyError initialError, + LoadingState? loadingState, + required WorkspaceState workspaceState, + }) = _WorkspaceErrorState; + + factory WorkspaceErrorState.initial(FlowyError error) => WorkspaceErrorState( + initialError: error, + workspaceState: const WorkspaceState.initial(), + ); +} + +@freezed +class WorkspaceState with _$WorkspaceState { + const factory WorkspaceState.initial() = _Initial; + const factory WorkspaceState.logout() = _Logout; + const factory WorkspaceState.reset() = _Reset; + const factory WorkspaceState.createNewWorkspace() = _NewWorkspace; + const factory WorkspaceState.restoreFromSnapshot() = _RestoreFromSnapshot; +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/empty_workspace_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/empty_workspace_screen.dart deleted file mode 100644 index 07cc471f7f..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/empty_workspace_screen.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class EmptyWorkspaceScreen extends StatelessWidget { - const EmptyWorkspaceScreen({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/encrypt_secret_screen.dart index 3b46d46891..70273d8ce0 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/encrypt_secret_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/encrypt_secret_screen.dart @@ -1,8 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/user/presentation/sign_in_screen.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -49,9 +48,8 @@ class _EncryptSecretScreenState extends State { (unit) async { await runAppFlowy(); }, - (err) { - Log.error(err); - showSnackBarMessage(context, err.msg); + (error) { + handleOpenWorkspaceError(context, error); }, ); }, diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index c709993915..e66c24be72 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -6,6 +6,7 @@ import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; import 'package:appflowy/user/presentation/welcome_screen.dart'; import 'package:appflowy/workspace/presentation/home/home_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra_ui/widget/route/animation.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' @@ -14,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; import 'package:flutter/material.dart'; import 'encrypt_secret_screen.dart'; +import 'workspace_error_screen.dart'; const routerNameRoot = '/'; const routerNameSignUp = '/signUp'; @@ -88,6 +90,24 @@ class AuthRouter { ), ); } + + Future pushWorkspaceErrorScreen( + BuildContext context, + UserFolderPB userFolder, + FlowyError error, + ) async { + final screen = WorkspaceErrorScreen( + userFolder: userFolder, + error: error, + ); + await Navigator.of(context).push( + PageRoutes.fade( + () => screen, + const RouteSettings(name: routerNameWelcome), + RouteDurations.slow.inMilliseconds * .001, + ), + ); + } } class SplashRoute { diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart index 6f56f50836..56a92a1750 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart @@ -3,10 +3,14 @@ import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/historical_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/widgets/background.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; @@ -14,7 +18,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dartz/dartz.dart'; @@ -62,7 +65,26 @@ class SignInScreen extends StatelessWidget { router.pushHomeScreen(context, user); } }, - (error) => showSnapBar(context, error.msg), + (error) { + handleOpenWorkspaceError(context, error); + }, + ); + } +} + +void handleOpenWorkspaceError(BuildContext context, FlowyError error) { + if (error.code == ErrorCode.WorkspaceDataNotSync) { + final userFolder = UserFolderPB.fromBuffer(error.payload); + getIt().pushWorkspaceErrorScreen(context, userFolder, error); + } else { + Log.error(error); + showSnapBar( + context, + error.msg, + onClosed: () { + getIt().signOut(); + runAppFlowy(); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart index b4d5d20131..b9789463ab 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart @@ -1,5 +1,6 @@ import 'package:appflowy/env/env.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/presentation/sign_in_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; @@ -80,9 +81,8 @@ class SplashScreen extends StatelessWidget { workspaceSetting, ); }, - (error) async { - Log.error(error); - getIt().pushWelcomeScreen(context, userProfile); + (error) { + handleOpenWorkspaceError(context, error); }, ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/workspace_error_screen.dart new file mode 100644 index 0000000000..6e3527ce42 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/workspace_error_screen.dart @@ -0,0 +1,196 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../application/workspace_error_bloc.dart'; + +class WorkspaceErrorScreen extends StatelessWidget { + final FlowyError error; + final UserFolderPB userFolder; + const WorkspaceErrorScreen({ + required this.userFolder, + required this.error, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, + body: BlocProvider( + create: (context) => WorkspaceErrorBloc( + userFolder: userFolder, + error: error, + )..add(const WorkspaceErrorEvent.init()), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (previous, current) => + previous.workspaceState != current.workspaceState, + listener: (context, state) async { + await state.workspaceState.when( + initial: () {}, + logout: () async { + await getIt().signOut(); + await runAppFlowy(); + }, + reset: () async { + await getIt().signOut(); + await runAppFlowy(); + }, + restoreFromSnapshot: () {}, + createNewWorkspace: () {}, + ); + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.loadingState != current.loadingState, + listener: (context, state) async { + state.loadingState?.when( + loading: () {}, + finish: (error) { + error.fold( + (_) {}, + (err) { + showSnapBar(context, err.msg); + }, + ); + }, + ); + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + final List children = [ + WorkspaceErrorDescription(error: error), + ]; + + children.addAll([ + const VSpace(50), + const LogoutButton(), + const VSpace(20), + const ResetWorkspaceButton(), + ]); + + return Center( + child: SizedBox( + width: 500, + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: children, + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} + +class WorkspaceErrorDescription extends StatelessWidget { + final FlowyError error; + const WorkspaceErrorDescription({ + required this.error, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + state.initialError.msg.toString(), + fontSize: 14, + maxLines: 10, + ), + FlowyText.medium( + "Error code: ${state.initialError.code.value.toString()}", + fontSize: 12, + maxLines: 1, + ) + ], + ); + }, + ); + } +} + +class LogoutButton extends StatelessWidget { + const LogoutButton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + width: 200, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.settings_menu_logout.tr(), + textAlign: TextAlign.center, + ), + onTap: () async { + context.read().add( + const WorkspaceErrorEvent.logout(), + ); + }, + ), + ); + } +} + +class ResetWorkspaceButton extends StatelessWidget { + const ResetWorkspaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200, + height: 40, + child: BlocBuilder( + builder: (context, state) { + final isLoading = state.loadingState?.isLoading() ?? false; + final icon = isLoading + ? const Center( + child: CircularProgressIndicator.adaptive(), + ) + : null; + + return FlowyButton( + text: FlowyText.medium( + LocaleKeys.workspace_reset.tr(), + textAlign: TextAlign.center, + ), + onTap: () { + NavigatorAlertDialog( + title: LocaleKeys.workspace_resetWorkspacePrompt.tr(), + confirm: () { + context.read().add( + const WorkspaceErrorEvent.resetWorkspace(), + ); + }, + ).show(context); + }, + rightIcon: icon, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index adcf8de3f7..cb4fe2472d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -127,8 +127,8 @@ class _CreateFlowyAlertDialog extends State { ...[ ConstrainedBox( constraints: const BoxConstraints( - maxWidth: 300, - maxHeight: 100, + maxWidth: 400, + maxHeight: 260, ), child: FlowyText.medium( widget.title, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index dfa12313a3..63683a0ad4 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart @@ -1,26 +1,26 @@ +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -void showSnapBar(BuildContext context, String title, [Color? backgroundColor]) { +void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context) .showSnackBar( SnackBar( + duration: const Duration(milliseconds: 10000), content: WillPopScope( onWillPop: () async { ScaffoldMessenger.of(context).removeCurrentSnackBar(); return true; }, - child: Text( + child: FlowyText.medium( title, - style: const TextStyle( - color: Colors.black, - ), + fontSize: 16, ), ), - backgroundColor: backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, ), ) .closed - .then((value) => null); + .then((value) => onClosed?.call()); } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f34005db4c..a21a0b9bff 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -45,6 +45,8 @@ }, "workspace": { "create": "Create workspace", + "reset": "Reset workspace", + "resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace", "hint": "workspace", "notFoundError": "Workspace not found" }, diff --git a/frontend/rust-lib/flowy-config/src/event_handler.rs b/frontend/rust-lib/flowy-config/src/event_handler.rs index 9113accf9b..46cd1262c3 100644 --- a/frontend/rust-lib/flowy-config/src/event_handler.rs +++ b/frontend/rust-lib/flowy-config/src/event_handler.rs @@ -29,7 +29,7 @@ pub(crate) async fn get_key_value_handler( data: AFPluginData, ) -> DataResult { match store_preferences.upgrade() { - None => Err(FlowyError::internal().context("The store preferences is already drop"))?, + None => Err(FlowyError::internal().with_context("The store preferences is already drop"))?, Some(store_preferences) => { let data = data.into_inner(); let value = store_preferences.get_str(&data.key); @@ -46,7 +46,7 @@ pub(crate) async fn remove_key_value_handler( data: AFPluginData, ) -> FlowyResult<()> { match store_preferences.upgrade() { - None => Err(FlowyError::internal().context("The store preferences is already drop"))?, + None => Err(FlowyError::internal().with_context("The store preferences is already drop"))?, Some(store_preferences) => { let data = data.into_inner(); store_preferences.remove(&data.key); diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index 21310fa603..6079cc25f7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -35,7 +35,7 @@ impl DatabaseUser for DatabaseUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .user_id() } @@ -43,7 +43,7 @@ impl DatabaseUser for DatabaseUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .token() } @@ -51,7 +51,7 @@ impl DatabaseUser for DatabaseUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .get_collab_db(uid) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index b26d009ca4..d1df34fd5d 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -32,7 +32,7 @@ impl DocumentUser for DocumentUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .user_id() } @@ -40,7 +40,7 @@ impl DocumentUser for DocumentUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .token() } @@ -48,7 +48,7 @@ impl DocumentUser for DocumentUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .get_collab_db(uid) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index 6843e9bdfc..f08477931b 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -69,7 +69,7 @@ impl FolderUser for FolderUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .user_id() } @@ -77,7 +77,7 @@ impl FolderUser for FolderUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .token() } @@ -85,7 +85,7 @@ impl FolderUser for FolderUserImpl { self .0 .upgrade() - .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? .get_collab_db(uid) } } @@ -305,7 +305,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayout::Calendar => make_default_calendar(view_id, &name), ViewLayout::Document => { return FutureResult::new(async move { - Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout))) + Err(FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout))) }); }, }; @@ -332,7 +332,8 @@ impl FolderOperationHandler for DatabaseFolderOperation { _ => CSVFormat::Original, }; FutureResult::new(async move { - let content = String::from_utf8(bytes).map_err(|err| FlowyError::internal().context(err))?; + let content = + String::from_utf8(bytes).map_err(|err| FlowyError::internal().with_context(err))?; database_manager .import_csv(view_id, content, format) .await?; @@ -359,7 +360,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { let database_layout = match new.layout { ViewLayout::Document => { return FutureResult::new(async { - Err(FlowyError::internal().context("Can't handle document layout type")) + Err(FlowyError::internal().with_context("Can't handle document layout type")) }); }, ViewLayout::Grid => DatabaseLayoutPB::Grid, diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 6e58d84995..36e9c05d52 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -422,9 +422,9 @@ impl LocalServerDB for LocalServerDBImpl { fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result>, FlowyError> { let collab_db = open_collab_db(&self.storage_path, uid)?; let read_txn = collab_db.read_txn(); - let updates = read_txn - .get_all_updates(uid, object_id) - .map_err(|e| FlowyError::internal().context(format!("Failed to open collab db: {:?}", e)))?; + let updates = read_txn.get_all_updates(uid, object_id).map_err(|e| { + FlowyError::internal().with_context(format!("Failed to open collab db: {:?}", e)) + })?; Ok(updates) } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 46a7e65116..d72e3a7f34 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -22,7 +22,7 @@ fn upgrade_manager( ) -> FlowyResult> { let manager = database_manager .upgrade() - .ok_or(FlowyError::internal().context("The database manager is already dropped"))?; + .ok_or(FlowyError::internal().with_context("The database manager is already dropped"))?; Ok(manager) } @@ -459,7 +459,7 @@ pub(crate) async fn create_row_handler( .create_row(&view_id, group_id, params) .await? { - None => Err(FlowyError::internal().context("Create row fail")), + None => Err(FlowyError::internal().with_context("Create row fail")), Some(row) => data_result_ok(RowMetaPB::from(row.meta)), } } @@ -510,9 +510,10 @@ pub(crate) async fn new_select_option_handler( .create_select_option(¶ms.field_id, params.option_name) .await; match result { - None => { - Err(FlowyError::record_not_found().context("Create select option fail. Can't find the field")) - }, + None => Err( + FlowyError::record_not_found() + .with_context("Create select option fail. Can't find the field"), + ), Some(pb) => data_result_ok(pb), } } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index f3ecff6f3f..3c26065ae1 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -95,7 +95,7 @@ impl DatabaseManager { collab_raw_data = updates; }, Err(err) => { - return Err(FlowyError::record_not_found().context(format!( + return Err(FlowyError::record_not_found().with_context(format!( "get workspace database :{} failed: {}", database_storage_id, err, ))); @@ -156,7 +156,7 @@ impl DatabaseManager { let wdb = self.get_workspace_database().await?; wdb.get_database_id_with_view_id(view_id).ok_or_else(|| { FlowyError::record_not_found() - .context(format!("The database for view id: {} not found", view_id)) + .with_context(format!("The database for view id: {} not found", view_id)) }) } @@ -331,7 +331,7 @@ impl DatabaseManager { async fn get_workspace_database(&self) -> FlowyResult> { let database = self.workspace_database.read().await; match &*database { - None => Err(FlowyError::internal().context("Workspace database not initialized")), + None => Err(FlowyError::internal().with_context("Workspace database not initialized")), Some(user_database) => Ok(user_database.clone()), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs b/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs index b14c985395..366ab9b8ad 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs @@ -32,7 +32,7 @@ impl TypeCellData { pub fn from_json_str(s: &str) -> FlowyResult { let type_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| { let msg = format!("Deserialize {} to type cell data failed.{}", s, err); - FlowyError::internal().context(msg) + FlowyError::internal().with_context(msg) })?; Ok(type_cell_data) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index d67b0ebdc0..69e8c8e808 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -673,7 +673,7 @@ impl DatabaseEditor { Some(field) => Ok(field), None => { let msg = format!("Field with id:{} not found", &field_id); - Err(FlowyError::internal().context(msg)) + Err(FlowyError::internal().with_context(msg)) }, }?; (field, database.get_cell(field_id, &row_id).cell) @@ -767,7 +767,8 @@ impl DatabaseEditor { .fields .get_field(field_id) .ok_or_else(|| { - FlowyError::record_not_found().context(format!("Field with id:{} not found", &field_id)) + FlowyError::record_not_found() + .with_context(format!("Field with id:{} not found", &field_id)) })?; debug_assert!(FieldType::from(field.field_type).is_select_option()); @@ -802,7 +803,7 @@ impl DatabaseEditor { Some(field) => Ok(field), None => { let msg = format!("Field with id:{} not found", &field_id); - Err(FlowyError::internal().context(msg)) + Err(FlowyError::internal().with_context(msg)) }, }?; let mut type_option = select_type_option_from_field(&field)?; @@ -868,7 +869,8 @@ impl DatabaseEditor { .fields .get_field(field_id) .ok_or_else(|| { - FlowyError::record_not_found().context(format!("Field with id:{} not found", &field_id)) + FlowyError::record_not_found() + .with_context(format!("Field with id:{} not found", &field_id)) })?; debug_assert!(FieldType::from(field.field_type).is_checklist()); @@ -1047,11 +1049,10 @@ impl DatabaseEditor { &self, view_id: &str, ) -> FlowyResult { - let view = self - .database - .lock() - .get_view(view_id) - .ok_or_else(|| FlowyError::record_not_found().context("Can't find the database view"))?; + let view = + self.database.lock().get_view(view_id).ok_or_else(|| { + FlowyError::record_not_found().with_context("Can't find the database view") + })?; Ok(database_view_setting_pb_from_view(view)) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index f675fb9180..04498276f3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -418,7 +418,7 @@ impl DatabaseViewEditor { .as_ref() .and_then(|group| group.get_group(group_id)) { - None => Err(FlowyError::record_not_found().context("Can't find the group")), + None => Err(FlowyError::record_not_found().with_context("Can't find the group")), Some((_, group)) => Ok(GroupPB::from(group)), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index aa161b137e..acf9f7dd97 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -125,7 +125,10 @@ impl CellDataChangeset for RichTextTypeOption { _cell: Option, ) -> FlowyResult<(Cell, ::CellData)> { if changeset.len() > 10000 { - Err(FlowyError::text_too_long().context("The len of the text should not be more than 10000")) + Err( + FlowyError::text_too_long() + .with_context("The len of the text should not be more than 10000"), + ) } else { let text_cell_data = StrCellData(changeset); Ok((text_cell_data.clone().into(), text_cell_data)) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 2fefedd37d..3a65482174 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -223,7 +223,9 @@ where })?; Ok(()) }, - _ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")), + _ => Err( + FlowyError::record_not_found().with_context("Moving group failed. Groups are not exist"), + ), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs index e300add987..e341309445 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs @@ -33,7 +33,7 @@ impl CSVExport { .collect::>(); wtr .write_record(&field_records) - .map_err(|e| FlowyError::internal().context(e))?; + .map_err(|e| FlowyError::internal().with_context(e))?; // Write rows let mut field_by_field_id = IndexMap::new(); @@ -63,8 +63,8 @@ impl CSVExport { let data = wtr .into_inner() - .map_err(|e| FlowyError::internal().context(e))?; - let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().context(e))?; + .map_err(|e| FlowyError::internal().with_context(e))?; + let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().with_context(e))?; Ok(csv) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs index da69b75b9b..fb73bb2190 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs @@ -1,13 +1,15 @@ -use crate::entities::FieldType; +use std::{fs::File, io::prelude::*}; -use crate::services::field::{default_type_option_data_from_type, CELL_DATA}; -use crate::services::share::csv::CSVFormat; use collab_database::database::{gen_database_id, gen_field_id, gen_row_id}; use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, CreateRowParams}; use collab_database::views::{CreateDatabaseParams, DatabaseLayout}; + use flowy_error::{FlowyError, FlowyResult}; -use std::{fs::File, io::prelude::*}; + +use crate::entities::FieldType; +use crate::services::field::{default_type_option_data_from_type, CELL_DATA}; +use crate::services::share::csv::CSVFormat; #[derive(Default)] pub struct CSVImporter; @@ -41,7 +43,7 @@ impl CSVImporter { fn get_fields_and_rows(&self, content: String) -> Result { let mut fields: Vec = vec![]; if content.is_empty() { - return Err(FlowyError::invalid_data().context("Import content is empty")); + return Err(FlowyError::invalid_data().with_context("Import content is empty")); } let mut reader = csv::Reader::from_reader(content.as_bytes()); @@ -50,7 +52,7 @@ impl CSVImporter { fields.push(header.to_string()); } } else { - return Err(FlowyError::invalid_data().context("Header not found")); + return Err(FlowyError::invalid_data().with_context("Header not found")); } let rows = reader @@ -164,9 +166,10 @@ pub struct ImportResult { #[cfg(test)] mod tests { - use crate::services::share::csv::{CSVFormat, CSVImporter}; use collab_database::database::gen_database_view_id; + use crate::services::share::csv::{CSVFormat, CSVImporter}; + #[test] fn test_import_csv_from_str() { let s = r#"Name,Tags,Number,Date,Checkbox,URL diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index 3ec2da0924..89b8895500 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -22,7 +22,7 @@ fn upgrade_document( ) -> FlowyResult> { let manager = document_manager .upgrade() - .ok_or(FlowyError::internal().context("The document manager is already dropped"))?; + .ok_or(FlowyError::internal().with_context("The document manager is already dropped"))?; Ok(manager) } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index e405f12ca0..8a9b6fb5ce 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -3,9 +3,12 @@ use thiserror::Error; use flowy_derive::ProtoBuf_Enum; -#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize_repr, Deserialize_repr, ProtoBuf_Enum)] +#[derive( + Debug, Default, Clone, PartialEq, Eq, Error, Serialize_repr, Deserialize_repr, ProtoBuf_Enum, +)] #[repr(u8)] pub enum ErrorCode { + #[default] #[error("Internal error")] Internal = 0, @@ -226,6 +229,9 @@ pub enum ErrorCode { #[error("It appears that the collaboration object's data has not been fully synchronized")] CollabDataNotSync = 75, + + #[error("It appears that the workspace data has not been fully synchronized")] + WorkspaceDataNotSync = 76, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 071dbee298..dc99416125 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -1,6 +1,7 @@ +use std::convert::TryInto; use std::fmt::Debug; -use anyhow::Result; +use protobuf::ProtobufError; use thiserror::Error; use flowy_derive::ProtoBuf; @@ -13,10 +14,13 @@ pub type FlowyResult = anyhow::Result; #[error("{code:?}: {msg}")] pub struct FlowyError { #[pb(index = 1)] - pub code: i32, + pub code: ErrorCode, #[pb(index = 2)] pub msg: String, + + #[pb(index = 3)] + pub payload: Vec, } macro_rules! static_flowy_error { @@ -31,17 +35,23 @@ macro_rules! static_flowy_error { impl FlowyError { pub fn new(code: ErrorCode, msg: T) -> Self { Self { - code: code.value(), + code, msg: msg.to_string(), + payload: vec![], } } - pub fn context(mut self, error: T) -> Self { + pub fn with_context(mut self, error: T) -> Self { self.msg = format!("{:?}", error); self } + pub fn with_payload, Error = ProtobufError>>(mut self, payload: T) -> Self { + self.payload = payload.try_into().unwrap_or_default(); + self + } + pub fn is_record_not_found(&self) -> bool { - self.code == ErrorCode::RecordNotFound.value() + self.code == ErrorCode::RecordNotFound } static_flowy_error!(internal, ErrorCode::Internal); @@ -93,9 +103,11 @@ impl FlowyError { impl std::convert::From for FlowyError { fn from(code: ErrorCode) -> Self { + let msg = format!("{}", code); FlowyError { - code: code.value(), - msg: format!("{}", code), + code, + msg, + payload: vec![], } } } @@ -104,18 +116,18 @@ pub fn internal_error(e: T) -> FlowyError where T: std::fmt::Debug, { - FlowyError::internal().context(e) + FlowyError::internal().with_context(e) } impl std::convert::From for FlowyError { fn from(error: std::io::Error) -> Self { - FlowyError::internal().context(error) + FlowyError::internal().with_context(error) } } impl std::convert::From for FlowyError { fn from(e: protobuf::ProtobufError) -> Self { - FlowyError::internal().context(e) + FlowyError::internal().with_context(e) } } diff --git a/frontend/rust-lib/flowy-error/src/impl_from/collab.rs b/frontend/rust-lib/flowy-error/src/impl_from/collab.rs index ceee5ad3f1..f55c6f5440 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/collab.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/collab.rs @@ -1,15 +1,16 @@ -use crate::FlowyError; use collab_database::error::DatabaseError; use collab_document::error::DocumentError; +use crate::FlowyError; + impl From for FlowyError { fn from(error: DatabaseError) -> Self { - FlowyError::internal().context(error) + FlowyError::internal().with_context(error) } } impl From for FlowyError { fn from(error: DocumentError) -> Self { - FlowyError::internal().context(error) + FlowyError::internal().with_context(error) } } diff --git a/frontend/rust-lib/flowy-error/src/impl_from/database.rs b/frontend/rust-lib/flowy-error/src/impl_from/database.rs index b9db62e22d..3a72a7cdf3 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/database.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/database.rs @@ -2,12 +2,12 @@ use crate::FlowyError; impl std::convert::From for FlowyError { fn from(error: flowy_sqlite::Error) -> Self { - FlowyError::internal().context(error) + FlowyError::internal().with_context(error) } } impl std::convert::From<::r2d2::Error> for FlowyError { fn from(error: r2d2::Error) -> Self { - FlowyError::internal().context(error) + FlowyError::internal().with_context(error) } } diff --git a/frontend/rust-lib/flowy-error/src/impl_from/dispatch.rs b/frontend/rust-lib/flowy-error/src/impl_from/dispatch.rs index 83c733ef5f..eb211b24eb 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/dispatch.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/dispatch.rs @@ -1,7 +1,11 @@ -use crate::FlowyError; -use bytes::Bytes; -use lib_dispatch::prelude::{AFPluginEventResponse, ResponseBuilder}; use std::convert::TryInto; + +use bytes::Bytes; + +use lib_dispatch::prelude::{AFPluginEventResponse, ResponseBuilder}; + +use crate::FlowyError; + impl lib_dispatch::Error for FlowyError { fn as_response(&self) -> AFPluginEventResponse { let bytes: Bytes = self.clone().try_into().unwrap(); diff --git a/frontend/rust-lib/flowy-error/src/impl_from/postgres.rs b/frontend/rust-lib/flowy-error/src/impl_from/postgres.rs index b8e045a9d0..c5a79510dd 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/postgres.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/postgres.rs @@ -2,6 +2,6 @@ use crate::FlowyError; impl std::convert::From for FlowyError { fn from(error: tokio_postgres::Error) -> Self { - FlowyError::internal().context(error) + FlowyError::internal().with_context(error) } } diff --git a/frontend/rust-lib/flowy-error/src/impl_from/reqwest.rs b/frontend/rust-lib/flowy-error/src/impl_from/reqwest.rs index 4fc225e936..0856470d93 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/reqwest.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/reqwest.rs @@ -1,8 +1,9 @@ -use crate::FlowyError; use reqwest::Error; +use crate::FlowyError; + impl std::convert::From for FlowyError { fn from(error: Error) -> Self { - FlowyError::http().context(error) + FlowyError::http().with_context(error) } } diff --git a/frontend/rust-lib/flowy-error/src/impl_from/serde.rs b/frontend/rust-lib/flowy-error/src/impl_from/serde.rs index 333171bae1..64f5a98823 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/serde.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/serde.rs @@ -2,6 +2,6 @@ use crate::FlowyError; impl std::convert::From for FlowyError { fn from(error: serde_json::Error) -> Self { - FlowyError::serde().context(error) + FlowyError::serde().with_context(error) } } diff --git a/frontend/rust-lib/flowy-folder2/src/entities/import.rs b/frontend/rust-lib/flowy-folder2/src/entities/import.rs index 00ff772ef8..bf6c36123d 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/import.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/import.rs @@ -1,10 +1,10 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::FlowyError; + use crate::entities::parser::empty_str::NotEmptyStr; use crate::entities::ViewLayoutPB; use crate::share::{ImportParams, ImportType}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::FlowyError; - #[derive(Clone, Debug, ProtoBuf_Enum)] pub enum ImportTypePB { HistoryDocument = 0, @@ -69,7 +69,7 @@ impl TryInto for ImportPB { None => None, Some(file_path) => Some( NotEmptyStr::parse(file_path) - .map_err(|_| FlowyError::invalid_data().context("The import file path is empty"))? + .map_err(|_| FlowyError::invalid_data().with_context("The import file path is empty"))? .0, ), }; diff --git a/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs index 2d5e2ff29a..0889d878bf 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs @@ -1,12 +1,15 @@ +use std::convert::TryInto; + +use collab::core::collab_state::SyncState; +use collab_folder::core::Workspace; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + use crate::{ entities::parser::workspace::{WorkspaceDesc, WorkspaceIdentify, WorkspaceName}, entities::view::ViewPB, }; -use collab::core::collab_state::SyncState; -use collab_folder::core::Workspace; -use flowy_derive::ProtoBuf; -use flowy_error::ErrorCode; -use std::convert::TryInto; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] pub struct WorkspacePB { @@ -197,3 +200,12 @@ impl From for FolderSyncStatePB { } } } + +#[derive(ProtoBuf, Default)] +pub struct UserFolderPB { + #[pb(index = 1)] + pub uid: i64, + + #[pb(index = 2)] + pub workspace_id: String, +} diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index a70f708fba..fc586523a8 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -12,7 +12,7 @@ fn upgrade_folder( ) -> FlowyResult> { let folder = folder_manager .upgrade() - .ok_or(FlowyError::internal().context("The folder manager is already dropped"))?; + .ok_or(FlowyError::internal().with_context("The folder manager is already dropped"))?; Ok(folder) } @@ -45,10 +45,10 @@ pub(crate) async fn open_workspace_handler( let folder = upgrade_folder(folder)?; let params: WorkspaceIdPB = data.into_inner(); match params.value { - None => Err(FlowyError::workspace_id().context("workspace id should not be empty")), + None => Err(FlowyError::workspace_id().with_context("workspace id should not be empty")), Some(workspace_id) => { if workspace_id.is_empty() { - Err(FlowyError::workspace_id().context("workspace id should not be empty")) + Err(FlowyError::workspace_id().with_context("workspace id should not be empty")) } else { let workspace = folder.open_workspace(&workspace_id).await?; let views = folder.get_workspace_views(&workspace_id).await?; diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index b2f822dd0a..0b2aefce15 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -37,8 +37,8 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::RestoreAllTrash, restore_all_trash_handler) .event(FolderEvent::DeleteAllTrash, delete_all_trash_handler) .event(FolderEvent::ImportData, import_data_handler) - .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) - .event(FolderEvent::UpdateViewIcon, update_view_icon_handler) + .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) + .event(FolderEvent::UpdateViewIcon, update_view_icon_handler) .event(FolderEvent::ReadFavorites, read_favorites_handler) .event(FolderEvent::ToggleFavorite, toggle_favorites_handler) } @@ -134,7 +134,7 @@ pub enum FolderEvent { #[event(input = "ImportPB")] ImportData = 30, - #[event()] + #[event(input = "WorkspaceIdPB", output = "RepeatedFolderSnapshotPB")] GetFolderSnapshots = 31, /// Moves a nested view to a new location in the hierarchy. /// diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 8f732fb67d..243f200c02 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -10,7 +10,7 @@ use collab_folder::core::{ FavoritesInfo, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, }; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use tokio_stream::wrappers::WatchStream; use tokio_stream::StreamExt; use tracing::{event, Level}; @@ -22,7 +22,8 @@ use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams, CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB, - RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, ViewPB, WorkspacePB, + RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, UserFolderPB, ViewPB, + WorkspacePB, }; use crate::notification::{ send_notification, send_workspace_notification, send_workspace_setting_notification, @@ -42,6 +43,7 @@ pub trait FolderUser: Send + Sync { } pub struct FolderManager { + workspace_id: RwLock>, mutex_folder: Arc, collab_builder: Arc, user: Arc, @@ -66,6 +68,7 @@ impl FolderManager { collab_builder, operation_handlers, cloud_service, + workspace_id: Default::default(), }; Ok(manager) @@ -73,7 +76,14 @@ impl FolderManager { pub async fn get_current_workspace(&self) -> FlowyResult { self.with_folder( - Err(FlowyError::internal().context("Folder is not initialized".to_string())), + || { + let uid = self.user.user_id()?; + let workspace_id = self.workspace_id.read().as_ref().cloned().ok_or( + FlowyError::from(ErrorCode::WorkspaceIdInvalid) + .with_context("Unexpected empty workspace id"), + )?; + Err(workspace_data_not_sync_error(uid, &workspace_id)) + }, |folder| { let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { let views = get_workspace_view_pbs(&workspace.id, folder); @@ -87,7 +97,7 @@ impl FolderManager { // from the folder. Otherwise, return an error. let mut workspaces = folder.workspaces.get_all_workspaces(); if workspaces.is_empty() { - Err(FlowyError::record_not_found().context("Can not find the workspace")) + Err(FlowyError::record_not_found().with_context("Can not find the workspace")) } else { tracing::error!("Can't find the current workspace, use the first workspace"); let workspace = workspaces.remove(0); @@ -119,9 +129,10 @@ impl FolderManager { } pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult> { - let views = self.with_folder(vec![], |folder| { - get_workspace_view_pbs(workspace_id, folder) - }); + let views = self.with_folder( + || vec![], + |folder| get_workspace_view_pbs(workspace_id, folder), + ); Ok(views) } @@ -134,6 +145,7 @@ impl FolderManager { workspace_id: &str, initial_data: FolderInitializeData, ) -> FlowyResult<()> { + *self.workspace_id.write() = Some(workspace_id.to_string()); let workspace_id = workspace_id.to_string(); if let Ok(collab_db) = self.user.collab_db(uid) { let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); @@ -157,10 +169,7 @@ impl FolderManager { }, FolderInitializeData::Raw(raw_data) => { if raw_data.is_empty() { - return Err(FlowyError::new( - ErrorCode::CollabDataNotSync, - "Can't fetch the workspace from server", - )); + return Err(workspace_data_not_sync_error(uid, &workspace_id)); } let collab = self.collab_for_folder(uid, &workspace_id, collab_db, raw_data)?; Folder::open(collab, Some(folder_notifier)) @@ -299,10 +308,13 @@ impl FolderManager { .create_workspace(self.user.user_id()?, ¶ms.name) .await?; - self.with_folder((), |folder| { - folder.workspaces.create_workspace(workspace.clone()); - folder.set_current_workspace(&workspace.id); - }); + self.with_folder( + || (), + |folder| { + folder.workspaces.create_workspace(workspace.clone()); + folder.set_current_workspace(&workspace.id); + }, + ); let repeated_workspace = RepeatedWorkspacePB { items: vec![workspace.clone().into()], @@ -313,20 +325,26 @@ impl FolderManager { #[tracing::instrument(level = "info", skip_all, err)] pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult { - self.with_folder(Err(FlowyError::internal()), |folder| { - let workspace = folder - .workspaces - .get_workspace(workspace_id) - .ok_or_else(|| { - FlowyError::record_not_found().context("Can't open not existing workspace") - })?; - folder.set_current_workspace(&workspace.id); - Ok::(workspace) - }) + self.with_folder( + || Err(FlowyError::internal()), + |folder| { + let workspace = folder + .workspaces + .get_workspace(workspace_id) + .ok_or_else(|| { + FlowyError::record_not_found().with_context("Can't open not existing workspace") + })?; + folder.set_current_workspace(&workspace.id); + Ok::(workspace) + }, + ) } pub async fn get_workspace(&self, workspace_id: &str) -> Option { - self.with_folder(None, |folder| folder.workspaces.get_workspace(workspace_id)) + self.with_folder( + || None, + |folder| folder.workspaces.get_workspace(workspace_id), + ) } async fn get_current_workspace_id(&self) -> FlowyResult { @@ -335,22 +353,30 @@ impl FolderManager { .lock() .as_ref() .and_then(|folder| folder.get_current_workspace_id()) - .ok_or(FlowyError::internal().context("Unexpected empty workspace id")) + .ok_or(FlowyError::internal().with_context("Unexpected empty workspace id")) } - fn with_folder(&self, default_value: Output, f: F) -> Output + /// This function acquires a lock on the `mutex_folder` and checks its state. + /// If the folder is `None`, it invokes the `none_callback`, otherwise, it passes the folder to the `f2` callback. + /// + /// # Parameters + /// + /// * `none_callback`: A callback function that is invoked when `mutex_folder` contains `None`. + /// * `f2`: A callback function that is invoked when `mutex_folder` contains a `Some` value. The contained folder is passed as an argument to this callback. + fn with_folder(&self, none_callback: F1, f2: F2) -> Output where - F: FnOnce(&Folder) -> Output, + F1: FnOnce() -> Output, + F2: FnOnce(&Folder) -> Output, { let folder = self.mutex_folder.lock(); match &*folder { - None => default_value, - Some(folder) => f(folder), + None => none_callback(), + Some(folder) => f2(folder), } } pub async fn get_all_workspaces(&self) -> Vec { - self.with_folder(vec![], |folder| folder.workspaces.get_all_workspaces()) + self.with_folder(|| vec![], |folder| folder.workspaces.get_all_workspaces()) } pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult { @@ -381,9 +407,12 @@ impl FolderManager { let index = params.index; let view = create_view(params, view_layout); - self.with_folder((), |folder| { - folder.insert_view(view.clone(), index); - }); + self.with_folder( + || (), + |folder| { + folder.insert_view(view.clone(), index); + }, + ); Ok(view) } @@ -402,15 +431,18 @@ impl FolderManager { .create_built_in_view(user_id, ¶ms.view_id, ¶ms.name, view_layout.clone()) .await?; let view = create_view(params, view_layout); - self.with_folder((), |folder| { - folder.insert_view(view.clone(), None); - }); + self.with_folder( + || (), + |folder| { + folder.insert_view(view.clone(), None); + }, + ); Ok(view) } #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { - if let Some(view) = self.with_folder(None, |folder| folder.views.get_view(view_id)) { + if let Some(view) = self.with_folder(|| None, |folder| folder.views.get_view(view_id)) { let handler = self.get_handler(&view.layout)?; handler.close_view(view_id).await?; } @@ -455,24 +487,27 @@ impl FolderManager { /// All the favorite views being trashed will be unfavorited first to remove it from favorites list as well. The process of unfavoriting concerned view is handled by `unfavorite_view_and_decendants()` #[tracing::instrument(level = "debug", skip(self), err)] pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> { - self.with_folder((), |folder| { - if let Some(view) = folder.views.get_view(view_id) { - self.unfavorite_view_and_decendants(view.clone(), folder); - folder.add_trash(vec![view_id.to_string()]); - // notify the parent view that the view is moved to trash - send_notification(view_id, FolderNotification::DidMoveViewToTrash) - .payload(DeletedViewPB { - view_id: view_id.to_string(), - index: None, - }) - .send(); + self.with_folder( + || (), + |folder| { + if let Some(view) = folder.views.get_view(view_id) { + self.unfavorite_view_and_decendants(view.clone(), folder); + folder.add_trash(vec![view_id.to_string()]); + // notify the parent view that the view is moved to trash + send_notification(view_id, FolderNotification::DidMoveViewToTrash) + .payload(DeletedViewPB { + view_id: view_id.to_string(), + index: None, + }) + .send(); - notify_child_views_changed( - view_pb_without_child_views(view), - ChildViewChangeReason::DidDeleteView, - ); - } - }); + notify_child_views_changed( + view_pb_without_child_views(view), + ChildViewChangeReason::DidDeleteView, + ); + } + }, + ); Ok(()) } @@ -528,9 +563,12 @@ impl FolderManager { ) -> FlowyResult<()> { let view = self.get_view(&view_id).await?; let old_parent_id = view.parent_view_id; - self.with_folder((), |folder| { - folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); - }); + self.with_folder( + || (), + |folder| { + folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); + }, + ); notify_parent_view_did_change( self.mutex_folder.clone(), vec![new_parent_id, old_parent_id], @@ -574,9 +612,12 @@ impl FolderManager { if let (Some(actual_from_index), Some(actual_to_index)) = (actual_from_index, actual_to_index) { - self.with_folder((), |folder| { - folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); - }); + self.with_folder( + || (), + |folder| { + folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); + }, + ); notify_parent_view_did_change(self.mutex_folder.clone(), vec![parent_view_id]); } } @@ -587,9 +628,10 @@ impl FolderManager { /// Return a list of views that belong to the given parent view id. #[tracing::instrument(level = "debug", skip(self, parent_view_id), err)] pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult>> { - let views = self.with_folder(vec![], |folder| { - folder.views.get_views_belong_to(parent_view_id) - }); + let views = self.with_folder( + || vec![], + |folder| folder.views.get_views_belong_to(parent_view_id), + ); Ok(views) } @@ -625,8 +667,8 @@ impl FolderManager { #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> { let view = self - .with_folder(None, |folder| folder.views.get_view(view_id)) - .ok_or_else(|| FlowyError::record_not_found().context("Can't duplicate the view"))?; + .with_folder(|| None, |folder| folder.views.get_view(view_id)) + .ok_or_else(|| FlowyError::record_not_found().with_context("Can't duplicate the view"))?; let handler = self.get_handler(&view.layout)?; let view_data = handler.duplicate_view(&view.id).await?; @@ -670,22 +712,25 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_current_view(&self) -> Option { - let view_id = self.with_folder(None, |folder| folder.get_current_view())?; + let view_id = self.with_folder(|| None, |folder| folder.get_current_view())?; self.get_view(&view_id).await.ok() } /// Toggles the favorite status of a view identified by `view_id`If the view is not a favorite, it will be added to the favorites list; otherwise, it will be removed from the list. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn toggle_favorites(&self, view_id: &str) -> FlowyResult<()> { - self.with_folder((), |folder| { - if let Some(old_view) = folder.views.get_view(view_id) { - if old_view.is_favorite { - folder.delete_favorites(vec![view_id.to_string()]); - } else { - folder.add_favorites(vec![view_id.to_string()]); + self.with_folder( + || (), + |folder| { + if let Some(old_view) = folder.views.get_view(view_id) { + if old_view.is_favorite { + folder.delete_favorites(vec![view_id.to_string()]); + } else { + folder.add_favorites(vec![view_id.to_string()]); + } } - } - }); + }, + ); self.send_toggle_favorite_notification(view_id).await; Ok(()) } @@ -712,29 +757,35 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_favorites(&self) -> Vec { - self.with_folder(vec![], |folder| { - let trash_ids = folder - .get_all_trash() - .into_iter() - .map(|trash| trash.id) - .collect::>(); + self.with_folder( + || vec![], + |folder| { + let trash_ids = folder + .get_all_trash() + .into_iter() + .map(|trash| trash.id) + .collect::>(); - let mut views = folder.get_all_favorites(); - views.retain(|view| !trash_ids.contains(&view.id)); - views - }) + let mut views = folder.get_all_favorites(); + views.retain(|view| !trash_ids.contains(&view.id)); + views + }, + ) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_trash(&self) -> Vec { - self.with_folder(vec![], |folder| folder.get_all_trash()) + self.with_folder(|| vec![], |folder| folder.get_all_trash()) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn restore_all_trash(&self) { - self.with_folder((), |folder| { - folder.remote_all_trash(); - }); + self.with_folder( + || (), + |folder| { + folder.remote_all_trash(); + }, + ); send_notification("trash", FolderNotification::DidUpdateTrash) .payload(RepeatedTrashPB { items: vec![] }) .send(); @@ -742,15 +793,18 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn restore_trash(&self, trash_id: &str) { - self.with_folder((), |folder| { - folder.delete_trash(vec![trash_id.to_string()]); - }); + self.with_folder( + || (), + |folder| { + folder.delete_trash(vec![trash_id.to_string()]); + }, + ); } /// Delete all the trash permanently. #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn delete_all_trash(&self) { - let deleted_trash = self.with_folder(vec![], |folder| folder.get_all_trash()); + let deleted_trash = self.with_folder(|| vec![], |folder| folder.get_all_trash()); for trash in deleted_trash { let _ = self.delete_trash(&trash.id).await; } @@ -764,11 +818,14 @@ impl FolderManager { /// is a database view. Then the database will be deleted as well. #[tracing::instrument(level = "debug", skip(self, view_id), err)] pub async fn delete_trash(&self, view_id: &str) -> FlowyResult<()> { - let view = self.with_folder(None, |folder| folder.views.get_view(view_id)); - self.with_folder((), |folder| { - folder.delete_trash(vec![view_id.to_string()]); - folder.views.delete_views(vec![view_id]); - }); + let view = self.with_folder(|| None, |folder| folder.views.get_view(view_id)); + self.with_folder( + || (), + |folder| { + folder.delete_trash(vec![view_id.to_string()]); + folder.views.delete_views(vec![view_id]); + }, + ); if let Some(view) = view { if let Ok(handler) = self.get_handler(&view.layout) { handler.delete_view(view_id).await?; @@ -819,9 +876,12 @@ impl FolderManager { }; let view = create_view(params, import_data.view_layout); - self.with_folder((), |folder| { - folder.insert_view(view.clone(), None); - }); + self.with_folder( + || (), + |folder| { + folder.insert_view(view.clone(), None); + }, + ); notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]); Ok(view) } @@ -831,12 +891,15 @@ impl FolderManager { where F: FnOnce(ViewUpdate) -> Option, { - let value = self.with_folder(None, |folder| { - let old_view = folder.views.get_view(view_id); - let new_view = folder.views.update_view(view_id, f); + let value = self.with_folder( + || None, + |folder| { + let old_view = folder.views.get_view(view_id); + let new_view = folder.views.update_view(view_id, f); - Some((old_view, new_view)) - }); + Some((old_view, new_view)) + }, + ); if let Some((Some(old_view), Some(new_view))) = value { if let Ok(handler) = self.get_handler(&old_view.layout) { @@ -858,7 +921,7 @@ impl FolderManager { view_layout: &ViewLayout, ) -> FlowyResult> { match self.operation_handlers.get(view_layout) { - None => Err(FlowyError::internal().context(format!( + None => Err(FlowyError::internal().with_context(format!( "Get data processor failed. Unknown layout type: {:?}", view_layout ))), @@ -871,34 +934,37 @@ impl FolderManager { /// Otherwise, the parent_view_id is the parent view id of the view. The child_view_ids is the /// child view ids of the view. async fn get_view_relation(&self, view_id: &str) -> Option<(bool, String, Vec)> { - self.with_folder(None, |folder| { - let view = folder.views.get_view(view_id)?; - match folder.views.get_view(&view.parent_view_id) { - None => folder.get_current_workspace().map(|workspace| { - ( - true, - workspace.id, - workspace - .child_views + self.with_folder( + || None, + |folder| { + let view = folder.views.get_view(view_id)?; + match folder.views.get_view(&view.parent_view_id) { + None => folder.get_current_workspace().map(|workspace| { + ( + true, + workspace.id, + workspace + .child_views + .items + .into_iter() + .map(|view| view.id) + .collect::>(), + ) + }), + Some(parent_view) => Some(( + false, + parent_view.id.clone(), + parent_view + .children .items + .clone() .into_iter() .map(|view| view.id) .collect::>(), - ) - }), - Some(parent_view) => Some(( - false, - parent_view.id.clone(), - parent_view - .children - .items - .clone() - .into_iter() - .map(|view| view.id) - .collect::>(), - )), - } - }) + )), + } + }, + ) } pub async fn get_folder_snapshots( @@ -1158,7 +1224,7 @@ fn notify_child_views_changed(view_pb: ViewPB, reason: ChildViewChangeReason) { } fn folder_not_init_error() -> FlowyError { - FlowyError::internal().context("Folder not initialized") + FlowyError::internal().with_context("Folder not initialized") } #[derive(Clone, Default)] @@ -1190,3 +1256,10 @@ fn is_exist_in_local_disk(user: &Arc, doc_id: &str) -> FlowyResu Ok(false) } } + +fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> FlowyError { + FlowyError::from(ErrorCode::WorkspaceDataNotSync).with_payload(UserFolderPB { + uid, + workspace_id: workspace_id.to_string(), + }) +} diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 0dff269757..96dd5d7747 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -118,6 +118,10 @@ impl UserService for LocalServerUserAuthServiceImpl { FutureResult::new(async { Ok(vec![]) }) } + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } + fn create_collab_object( &self, _collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-server/src/request.rs b/frontend/rust-lib/flowy-server/src/request.rs index 49f0994345..f5c77720f4 100644 --- a/frontend/rust-lib/flowy-server/src/request.rs +++ b/frontend/rust-lib/flowy-server/src/request.rs @@ -159,7 +159,7 @@ impl HttpRequestBuilder { fn unexpected_empty_payload(url: &str) -> FlowyError { let msg = format!("Request: {} receives unexpected empty payload", url); - FlowyError::payload_none().context(msg) + FlowyError::payload_none().with_context(msg) } async fn flowy_response_from(original: Response) -> Result { @@ -178,7 +178,7 @@ async fn get_response_data(original: Response) -> Result { Some(error) => Err(FlowyError::new(error.code, &error.msg)), } } else { - Err(FlowyError::http().context(original)) + Err(FlowyError::http().with_context(original)) } } diff --git a/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs b/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs index 350d1df414..ee474f592f 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs @@ -129,6 +129,11 @@ impl UserService for SelfHostedUserAuthServiceImpl { FutureResult::new(async { Ok(vec![]) }) } + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> { + // TODO(nathan): implement the RESTful API for this + FutureResult::new(async { Ok(()) }) + } + fn create_collab_object( &self, _collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index 60ab6da2fd..94b56ce0ca 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -161,8 +161,7 @@ where .get_workspace_id() .ok_or(anyhow::anyhow!("Invalid workspace id"))?; - let update_items = - get_updates_from_server(&object.object_id, &object.ty, postgrest.clone()).await?; + let update_items = get_updates_from_server(&object.object_id, &object.ty, &postgrest).await?; // If the update_items is empty, we can send the init_update directly if update_items.is_empty() { @@ -175,32 +174,7 @@ where ) .await?; } else { - // 2.Merge the updates into one and then delete the merged updates - let merge_result = spawn_blocking(move || merge_updates(update_items, init_update)).await??; - tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len()); - - let value_size = merge_result.new_update.len() as i32; - let md5 = md5(&merge_result.new_update); - let (new_update, encrypt) = - SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &self.secret())?; - let params = InsertParamsBuilder::new() - .insert("oid", object.object_id.clone()) - .insert("new_value", new_update) - .insert("encrypt", encrypt) - .insert("md5", md5) - .insert("value_size", value_size) - .insert("partition_key", partition_key(&object.ty)) - .insert("uid", object.uid) - .insert("workspace_id", workspace_id) - .insert("removed_keys", merge_result.merged_keys) - .insert("did", object.get_device_id()) - .build(); - - postgrest - .rpc("flush_collab_updates_v3", params) - .execute() - .await? - .success() + flush_collab_with_update(object, update_items, &postgrest, init_update, self.secret()) .await?; } Ok(()) @@ -215,6 +189,49 @@ where } } +pub(crate) async fn flush_collab_with_update( + object: &CollabObject, + update_items: Vec, + postgrest: &Arc, + update: Vec, + secret: Option, +) -> Result<(), Error> { + // 2.Merge the updates into one and then delete the merged updates + let merge_result = spawn_blocking(move || merge_updates(update_items, update)).await??; + tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len()); + + let workspace_id = object + .get_workspace_id() + .ok_or(anyhow::anyhow!("Invalid workspace id"))?; + + let value_size = merge_result.new_update.len() as i32; + let md5 = md5(&merge_result.new_update); + + tracing::trace!("Flush collab id:{} type:{}", object.object_id, object.ty); + let (new_update, encrypt) = + SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &secret)?; + let params = InsertParamsBuilder::new() + .insert("oid", object.object_id.clone()) + .insert("new_value", new_update) + .insert("encrypt", encrypt) + .insert("md5", md5) + .insert("value_size", value_size) + .insert("partition_key", partition_key(&object.ty)) + .insert("uid", object.uid) + .insert("workspace_id", workspace_id) + .insert("removed_keys", merge_result.merged_keys) + .insert("did", object.get_device_id()) + .build(); + + postgrest + .rpc("flush_collab_updates_v3", params) + .execute() + .await? + .success() + .await?; + Ok(()) +} + pub(crate) async fn send_update( workspace_id: String, object: &CollabObject, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index 118c7b1bf4..039819e363 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -72,7 +72,7 @@ where let workspace_id = workspace_id.to_string(); FutureResult::new(async move { let postgrest = try_get_postgrest?; - let updates = get_updates_from_server(&workspace_id, &CollabType::Folder, postgrest).await?; + let updates = get_updates_from_server(&workspace_id, &CollabType::Folder, &postgrest).await?; let updates = updates .into_iter() .map(|item| item.value) diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs index f46f2fa6dd..690deb4722 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs @@ -66,12 +66,14 @@ impl Action for FetchObjectUpdateAction { Box::pin(async move { match weak_postgres.upgrade() { None => Ok(vec![]), - Some(postgrest) => match get_updates_from_server(&object_id, &object_ty, postgrest).await { - Ok(items) => Ok(items.into_iter().map(|item| item.value).collect()), - Err(err) => { - tracing::error!("Get {} updates failed with error: {:?}", object_id, err); - Err(err) - }, + Some(postgrest) => { + match get_updates_from_server(&object_id, &object_ty, &postgrest).await { + Ok(items) => Ok(items.into_iter().map(|item| item.value).collect()), + Err(err) => { + tracing::error!("Get {} updates failed with error: {:?}", object_id, err); + Err(err) + }, + } }, } }) @@ -285,7 +287,7 @@ pub async fn batch_get_updates_from_server( pub async fn get_updates_from_server( object_id: &str, object_ty: &CollabType, - postgrest: Arc, + postgrest: &Arc, ) -> Result, Error> { let json = postgrest .from(table_name(object_ty)) diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 5d627d4163..a0d44e1beb 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -2,23 +2,29 @@ use std::str::FromStr; use std::sync::{Arc, Weak}; use anyhow::Error; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; use collab_plugins::cloud_storage::CollabObject; use parking_lot::RwLock; use serde_json::Value; use tokio::sync::oneshot::channel; use uuid::Uuid; +use flowy_folder_deps::cloud::{Folder, Workspace}; use flowy_user_deps::cloud::*; use flowy_user_deps::entities::*; use flowy_user_deps::DEFAULT_USER_NAME; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; +use lib_infra::util::timestamp; -use crate::supabase::api::request::FetchObjectUpdateAction; +use crate::supabase::api::request::{get_updates_from_server, FetchObjectUpdateAction}; use crate::supabase::api::util::{ ExtendedResponse, InsertParamsBuilder, RealtimeBinaryColumnDecoder, SupabaseBinaryColumnDecoder, }; -use crate::supabase::api::{send_update, PostgresWrapper, SupabaseServerService}; +use crate::supabase::api::{ + flush_collab_with_update, send_update, PostgresWrapper, SupabaseServerService, +}; use crate::supabase::define::*; use crate::supabase::entities::UserProfileResponse; use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent}; @@ -266,6 +272,39 @@ where self.user_update_tx.as_ref().map(|tx| tx.subscribe()) } + fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error> { + let collab_object = collab_object.clone(); + + let try_get_postgrest = self.server.try_get_weak_postgrest(); + let (tx, rx) = channel(); + let init_update = empty_workspace_update(&collab_object); + tokio::spawn(async move { + tx.send( + async move { + let postgrest = try_get_postgrest? + .upgrade() + .ok_or(anyhow::anyhow!("postgrest is not available"))?; + + let updates = + get_updates_from_server(&collab_object.object_id, &collab_object.ty, &postgrest) + .await?; + + flush_collab_with_update( + &collab_object, + updates, + &postgrest, + init_update, + postgrest.secret(), + ) + .await?; + Ok(()) + } + .await, + ) + }); + FutureResult::new(async { rx.await? }) + } + fn create_collab_object( &self, collab_object: &CollabObject, @@ -516,3 +555,21 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler { } } } + +fn empty_workspace_update(collab_object: &CollabObject) -> Vec { + let workspace_id = collab_object.object_id.clone(); + let collab = Arc::new(MutexCollab::new( + CollabOrigin::Empty, + &collab_object.object_id, + vec![], + )); + let folder = Folder::create(collab.clone(), None, None); + folder.workspaces.create_workspace(Workspace { + id: workspace_id.clone(), + name: "My workspace".to_string(), + child_views: Default::default(), + created_at: timestamp(), + }); + folder.set_current_workspace(&workspace_id); + collab.encode_as_update_v1().0 +} diff --git a/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs b/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs index b65c0de861..a0ce97a8a4 100644 --- a/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs @@ -127,7 +127,7 @@ async fn delete_view_event_test() { .await .error() .unwrap(); - assert_eq!(error.code, ErrorCode::RecordNotFound.value()); + assert_eq!(error.code, ErrorCode::RecordNotFound); } #[tokio::test] @@ -150,7 +150,7 @@ async fn put_back_trash_event_test() { .await .error() .unwrap(); - assert_eq!(error.code, ErrorCode::RecordNotFound.value()); + assert_eq!(error.code, ErrorCode::RecordNotFound); let payload = TrashIdPB { id: view.id.clone(), @@ -480,7 +480,7 @@ async fn create_parent_view_with_invalid_name() { .error() .unwrap() .code, - code.value() + code ) } } diff --git a/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs index 0ac1737f14..074e86cdaa 100644 --- a/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs @@ -27,7 +27,7 @@ async fn sign_up_with_invalid_email() { .error() .unwrap() .code, - ErrorCode::EmailFormatInvalid.value() + ErrorCode::EmailFormatInvalid ); } } @@ -51,7 +51,7 @@ async fn sign_up_with_long_password() { .error() .unwrap() .code, - ErrorCode::PasswordTooLong.value() + ErrorCode::PasswordTooLong ); } @@ -76,7 +76,7 @@ async fn sign_in_with_invalid_email() { .error() .unwrap() .code, - ErrorCode::EmailFormatInvalid.value() + ErrorCode::EmailFormatInvalid ); } } diff --git a/frontend/rust-lib/flowy-test/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/flowy-test/tests/user/local_test/user_profile_test.rs index 1dc9eddb13..43f7cdf325 100644 --- a/frontend/rust-lib/flowy-test/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/local_test/user_profile_test.rs @@ -79,7 +79,7 @@ async fn user_update_with_invalid_email() { .error() .unwrap() .code, - ErrorCode::EmailFormatInvalid.value() + ErrorCode::EmailFormatInvalid ); } } diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs index b3892f7f77..cb539883c7 100644 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs @@ -103,7 +103,7 @@ async fn third_party_sign_up_with_duplicated_email() { .await .err() .unwrap(); - assert_eq!(error.code, ErrorCode::Conflict.value()); + assert_eq!(error.code, ErrorCode::Conflict); }; } @@ -198,7 +198,7 @@ async fn check_not_exist_user_test() { .check_user_with_uuid(&uuid::Uuid::new_v4().to_string()) .await .unwrap_err(); - assert_eq!(err.code, ErrorCode::RecordNotFound.value()); + assert_eq!(err.code, ErrorCode::RecordNotFound); } } @@ -256,6 +256,6 @@ async fn update_user_profile_with_existing_email_test() { ) .await .unwrap(); - assert_eq!(error.code, ErrorCode::Conflict.value()); + assert_eq!(error.code, ErrorCode::Conflict); } } diff --git a/frontend/rust-lib/flowy-user-deps/src/cloud.rs b/frontend/rust-lib/flowy-user-deps/src/cloud.rs index 8400a865f4..787d450f86 100644 --- a/frontend/rust-lib/flowy-user-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-deps/src/cloud.rs @@ -110,6 +110,8 @@ pub trait UserService: Send + Sync { None } + fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error>; + fn create_collab_object( &self, collab_object: &CollabObject, diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index e3fab14283..d37ea79102 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -48,6 +48,9 @@ pub struct UserProfilePB { #[pb(index = 9)] pub encryption_type: EncryptionTypePB, + + #[pb(index = 10)] + pub workspace_id: String, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -78,6 +81,7 @@ impl std::convert::From for UserProfilePB { auth_type: user_profile.auth_type.into(), encryption_sign, encryption_type: encryption_ty, + workspace_id: user_profile.workspace_id, } } } @@ -274,3 +278,12 @@ impl From for HistoricalUserPB { } } } + +#[derive(ProtoBuf, Default, Clone)] +pub struct ResetWorkspacePB { + #[pb(index = 1)] + pub uid: i64, + + #[pb(index = 2)] + pub workspace_id: String, +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 848ba73859..bb97323f5b 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -3,7 +3,7 @@ use std::{convert::TryInto, sync::Arc}; use serde_json::Value; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::StorePreferences; use flowy_user_deps::cloud::UserCloudConfig; use flowy_user_deps::entities::*; @@ -20,7 +20,7 @@ use crate::services::cloud_config::{ fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { let manager = manager .upgrade() - .ok_or(FlowyError::internal().context("The user session is already drop"))?; + .ok_or(FlowyError::internal().with_context("The user session is already drop"))?; Ok(manager) } @@ -29,7 +29,7 @@ fn upgrade_store_preferences( ) -> FlowyResult> { let store = store .upgrade() - .ok_or(FlowyError::internal().context("The store preferences is already drop"))?; + .ok_or(FlowyError::internal().with_context("The store preferences is already drop"))?; Ok(store) } @@ -96,7 +96,15 @@ pub async fn get_user_profile_handler( let manager = upgrade_manager(manager)?; let uid = manager.get_session()?.user_id; let user_profile = manager.get_user_profile(uid).await?; - let _ = manager.refresh_user_profile(&user_profile).await; + + let weak_manager = Arc::downgrade(&manager); + let cloned_user_profile = user_profile.clone(); + tokio::spawn(async move { + if let Some(manager) = weak_manager.upgrade() { + let _ = manager.refresh_user_profile(&cloned_user_profile).await; + } + }); + data_result_ok(user_profile.into()) } @@ -250,7 +258,7 @@ pub async fn set_cloud_config_handler( let update = data.into_inner(); let store_preferences = upgrade_store_preferences(store_preferences)?; let mut config = get_cloud_config(session.user_id, &store_preferences) - .ok_or(FlowyError::internal().context("Can't find any cloud config"))?; + .ok_or(FlowyError::internal().with_context("Can't find any cloud config"))?; if let Some(enable_sync) = update.enable_sync { manager.cloud_services.set_enable_sync(enable_sync); @@ -429,3 +437,20 @@ pub async fn get_all_reminder_event_handler( .collect::>(); data_result_ok(reminders.into()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn reset_workspace_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let reset_pb = data.into_inner(); + if reset_pb.workspace_id.is_empty() { + return Err(FlowyError::new( + ErrorCode::WorkspaceIdInvalid, + "The workspace id is empty", + )); + } + manager.reset_workspace(reset_pb).await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 56a0ce909c..ea72cb0ed5 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -54,6 +54,7 @@ pub fn init(user_session: Weak) -> AFPlugin { .event(UserEvent::PushRealtimeEvent, push_realtime_event_handler) .event(UserEvent::CreateReminder, create_reminder_event_handler) .event(UserEvent::GetAllReminders, get_all_reminder_event_handler) + .event(UserEvent::ResetWorkspace, reset_workspace_handler) } pub struct SignUpContext { @@ -271,4 +272,7 @@ pub enum UserEvent { #[event(output = "RepeatedReminderPB")] GetAllReminders = 29, + + #[event(input = "ResetWorkspacePB")] + ResetWorkspace = 30, } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index 77f1bd7ddf..eada8dfbb4 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -70,7 +70,7 @@ pub fn open_user_db(root: &str, user_id: i64) -> Result, Flo let dir = user_db_path_from_uid(root, user_id); tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir); let db = flowy_sqlite::init(&dir) - .map_err(|e| FlowyError::internal().context(format!("open user db failed, {:?}", e)))?; + .map_err(|e| FlowyError::internal().with_context(format!("open user db failed, {:?}", e)))?; let pool = db.get_pool(); write_guard.insert(user_id.to_owned(), db); drop(write_guard); diff --git a/frontend/rust-lib/flowy-user/src/services/user_workspace.rs b/frontend/rust-lib/flowy-user/src/services/user_workspace.rs index fafe5dcfe8..985e9685bf 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_workspace.rs @@ -1,12 +1,14 @@ use std::convert::TryFrom; use std::sync::Arc; +use appflowy_integrate::{CollabObject, CollabType}; + use flowy_error::{FlowyError, FlowyResult}; use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::{query_dsl::*, ConnectionPool, ExpressionMethods}; use flowy_user_deps::entities::UserWorkspace; -use crate::entities::RepeatedUserWorkspacePB; +use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB}; use crate::manager::UserManager; use crate::notification::{send_notification, UserNotification}; use crate::services::user_workspace_sql::UserWorkspaceTable; @@ -84,6 +86,20 @@ impl UserManager { } Ok(rows.into_iter().map(UserWorkspace::from).collect()) } + + /// Reset the remote workspace using local workspace data. This is useful when a user wishes to + /// open a workspace on a new device that hasn't fully synchronized with the server. + pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> { + let collab_object = + CollabObject::new(reset.uid, reset.workspace_id.clone(), CollabType::Folder) + .with_workspace_id(reset.workspace_id); + self + .cloud_services + .get_user_service()? + .reset_workspace(collab_object) + .await?; + Ok(()) + } } pub fn save_user_workspaces( diff --git a/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs index 931adcd6b1..457fd24d9a 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs @@ -21,10 +21,10 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { fn try_from(value: (i64, &UserWorkspace)) -> Result { if value.1.id.is_empty() { - return Err(FlowyError::invalid_data().context("The id is empty")); + return Err(FlowyError::invalid_data().with_context("The id is empty")); } if value.1.database_storage_id.is_empty() { - return Err(FlowyError::invalid_data().context("The database storage id is empty")); + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); } Ok(Self { diff --git a/shared-lib/flowy-derive/src/proto_buf/serialize.rs b/shared-lib/flowy-derive/src/proto_buf/serialize.rs index b44e192576..f850a155aa 100644 --- a/shared-lib/flowy-derive/src/proto_buf/serialize.rs +++ b/shared-lib/flowy-derive/src/proto_buf/serialize.rs @@ -28,6 +28,16 @@ pub fn make_se_token_stream(ast_result: &ASTResult, ast: &ASTContainer) -> Optio } } + impl std::convert::TryInto> for #struct_ident { + type Error = ::protobuf::ProtobufError; + fn try_into(self) -> Result, Self::Error> { + use protobuf::Message; + let pb: crate::protobuf::#pb_ty = self.into(); + let bytes = pb.write_to_bytes()?; + Ok(bytes) + } + } + impl std::convert::From<#struct_ident> for crate::protobuf::#pb_ty { fn from(mut o: #struct_ident) -> crate::protobuf::#pb_ty { let mut pb = crate::protobuf::#pb_ty::new();