chore: optimize the UI if fail to open the workspace (#3246)

* chore: async load user profile

* chore: enable reset workspace

* chore: add confirm dialog
This commit is contained in:
Nathan.fooo 2023-08-22 00:19:15 +08:00 committed by GitHub
parent bd30e31f6c
commit 12d6cbd46a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 928 additions and 322 deletions

View File

@ -38,4 +38,7 @@ class LoadingState with _$LoadingState {
const factory LoadingState.finish( const factory LoadingState.finish(
Either<Unit, FlowyError> successOrFail, Either<Unit, FlowyError> successOrFail,
) = _Finish; ) = _Finish;
const LoadingState._();
isLoading() => this is _Loading;
} }

View File

@ -62,7 +62,7 @@ class RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {
viewsOrError.fold( viewsOrError.fold(
(view) => add(RowDocumentEvent.didReceiveRowDocument(view)), (view) => add(RowDocumentEvent.didReceiveRowDocument(view)),
(error) async { (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 // By default, the document of the row is not exist. So creating a
// new document for the given document id of the row. // new document for the given document id of the row.
final documentView = final documentView =

View File

@ -107,7 +107,7 @@ class DateCellCalendarBloc
} }
}, },
(err) { (err) {
switch (ErrorCode.valueOf(err.code)!) { switch (err.code) {
case ErrorCode.InvalidDateTimeFormat: case ErrorCode.InvalidDateTimeFormat:
if (isClosed) return; if (isClosed) return;
add( add(

View File

@ -1,5 +1,6 @@
import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/user_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:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -81,7 +82,7 @@ class AppFlowyAuthService implements AuthService {
}) async { }) async {
return left( return left(
FlowyError.create() FlowyError.create()
..code = 0 ..code = ErrorCode.Internal
..msg = "Unsupported sign up action", ..msg = "Unsupported sign up action",
); );
} }
@ -98,7 +99,7 @@ class AppFlowyAuthService implements AuthService {
}) async { }) async {
return left( return left(
FlowyError.create() FlowyError.create()
..code = 0 ..code = ErrorCode.Internal
..msg = "Unsupported sign up action", ..msg = "Unsupported sign up action",
); );
} }

View File

@ -1,19 +1,20 @@
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
class AuthError { class AuthError {
static final supabaseSignInError = FlowyError() static final supabaseSignInError = FlowyError()
..msg = 'supabase sign in error' ..msg = 'supabase sign in error -10001'
..code = -10001; ..code = ErrorCode.UserUnauthorized;
static final supabaseSignUpError = FlowyError() static final supabaseSignUpError = FlowyError()
..msg = 'supabase sign up error' ..msg = 'supabase sign up error -10002'
..code = -10002; ..code = ErrorCode.UserUnauthorized;
static final supabaseSignInWithOauthError = FlowyError() static final supabaseSignInWithOauthError = FlowyError()
..msg = 'supabase sign in with oauth error' ..msg = 'supabase sign in with oauth error -10003'
..code = -10003; ..code = ErrorCode.UserUnauthorized;
static final supabaseGetUserError = FlowyError() static final supabaseGetUserError = FlowyError()
..msg = 'unable to get user from supabase' ..msg = 'unable to get user from supabase -10004'
..code = -10004; ..code = ErrorCode.UserUnauthorized;
} }

View File

@ -156,7 +156,7 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
} }
SignInState stateFromCode(FlowyError error) { SignInState stateFromCode(FlowyError error) {
switch (ErrorCode.valueOf(error.code)) { switch (error.code) {
case ErrorCode.EmailFormatInvalid: case ErrorCode.EmailFormatInvalid:
return state.copyWith( return state.copyWith(
isSubmitting: false, isSubmitting: false,

View File

@ -119,7 +119,7 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
} }
SignUpState stateFromCode(FlowyError error) { SignUpState stateFromCode(FlowyError error) {
switch (ErrorCode.valueOf(error.code)!) { switch (error.code) {
case ErrorCode.EmailFormatInvalid: case ErrorCode.EmailFormatInvalid:
return state.copyWith( return state.copyWith(
isSubmitting: false, isSubmitting: false,

View File

@ -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<WorkspaceErrorEvent, WorkspaceErrorState> {
final UserFolderPB userFolder;
WorkspaceErrorBloc({
required this.userFolder,
required FlowyError error,
}) : super(WorkspaceErrorState.initial(error)) {
on<WorkspaceErrorEvent>((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<Unit, FlowyError> 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;
}

View File

@ -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();
}
}

View File

@ -1,8 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.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/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -49,9 +48,8 @@ class _EncryptSecretScreenState extends State<EncryptSecretScreen> {
(unit) async { (unit) async {
await runAppFlowy(); await runAppFlowy();
}, },
(err) { (error) {
Log.error(err); handleOpenWorkspaceError(context, error);
showSnackBarMessage(context, err.msg);
}, },
); );
}, },

View File

@ -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/user/presentation/welcome_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_screen.dart'; import 'package:appflowy/workspace/presentation/home/home_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.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/time/duration.dart';
import 'package:flowy_infra_ui/widget/route/animation.dart'; import 'package:flowy_infra_ui/widget/route/animation.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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 'package:flutter/material.dart';
import 'encrypt_secret_screen.dart'; import 'encrypt_secret_screen.dart';
import 'workspace_error_screen.dart';
const routerNameRoot = '/'; const routerNameRoot = '/';
const routerNameSignUp = '/signUp'; const routerNameSignUp = '/signUp';
@ -88,6 +90,24 @@ class AuthRouter {
), ),
); );
} }
Future<void> 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 { class SplashRoute {

View File

@ -3,10 +3,14 @@ import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/startup.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/historical_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/background.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:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.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_button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
@ -62,7 +65,26 @@ class SignInScreen extends StatelessWidget {
router.pushHomeScreen(context, user); 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<AuthRouter>().pushWorkspaceErrorScreen(context, userFolder, error);
} else {
Log.error(error);
showSnapBar(
context,
error.msg,
onClosed: () {
getIt<AuthService>().signOut();
runAppFlowy();
},
); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:appflowy/env/env.dart'; import 'package:appflowy/env/env.dart';
import 'package:appflowy/user/application/auth/auth_service.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/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -80,9 +81,8 @@ class SplashScreen extends StatelessWidget {
workspaceSetting, workspaceSetting,
); );
}, },
(error) async { (error) {
Log.error(error); handleOpenWorkspaceError(context, error);
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
}, },
); );
} }

View File

@ -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<WorkspaceErrorBloc, WorkspaceErrorState>(
listenWhen: (previous, current) =>
previous.workspaceState != current.workspaceState,
listener: (context, state) async {
await state.workspaceState.when(
initial: () {},
logout: () async {
await getIt<AuthService>().signOut();
await runAppFlowy();
},
reset: () async {
await getIt<AuthService>().signOut();
await runAppFlowy();
},
restoreFromSnapshot: () {},
createNewWorkspace: () {},
);
},
),
BlocListener<WorkspaceErrorBloc, WorkspaceErrorState>(
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<WorkspaceErrorBloc, WorkspaceErrorState>(
builder: (context, state) {
final List<Widget> 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<WorkspaceErrorBloc, WorkspaceErrorState>(
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<WorkspaceErrorBloc>().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<WorkspaceErrorBloc, WorkspaceErrorState>(
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<WorkspaceErrorBloc>().add(
const WorkspaceErrorEvent.resetWorkspace(),
);
},
).show(context);
},
rightIcon: icon,
);
},
),
);
}
}

View File

@ -127,8 +127,8 @@ class _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> {
...[ ...[
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 300, maxWidth: 400,
maxHeight: 100, maxHeight: 260,
), ),
child: FlowyText.medium( child: FlowyText.medium(
widget.title, widget.title,

View File

@ -1,26 +1,26 @@
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.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).clearSnackBars();
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar( .showSnackBar(
SnackBar( SnackBar(
duration: const Duration(milliseconds: 10000),
content: WillPopScope( content: WillPopScope(
onWillPop: () async { onWillPop: () async {
ScaffoldMessenger.of(context).removeCurrentSnackBar(); ScaffoldMessenger.of(context).removeCurrentSnackBar();
return true; return true;
}, },
child: Text( child: FlowyText.medium(
title, title,
style: const TextStyle( fontSize: 16,
color: Colors.black,
), ),
), ),
), backgroundColor: Theme.of(context).colorScheme.background,
backgroundColor: backgroundColor,
), ),
) )
.closed .closed
.then((value) => null); .then((value) => onClosed?.call());
} }

View File

@ -45,6 +45,8 @@
}, },
"workspace": { "workspace": {
"create": "Create 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", "hint": "workspace",
"notFoundError": "Workspace not found" "notFoundError": "Workspace not found"
}, },

View File

@ -29,7 +29,7 @@ pub(crate) async fn get_key_value_handler(
data: AFPluginData<KeyPB>, data: AFPluginData<KeyPB>,
) -> DataResult<KeyValuePB, FlowyError> { ) -> DataResult<KeyValuePB, FlowyError> {
match store_preferences.upgrade() { 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) => { Some(store_preferences) => {
let data = data.into_inner(); let data = data.into_inner();
let value = store_preferences.get_str(&data.key); let value = store_preferences.get_str(&data.key);
@ -46,7 +46,7 @@ pub(crate) async fn remove_key_value_handler(
data: AFPluginData<KeyPB>, data: AFPluginData<KeyPB>,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
match store_preferences.upgrade() { 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) => { Some(store_preferences) => {
let data = data.into_inner(); let data = data.into_inner();
store_preferences.remove(&data.key); store_preferences.remove(&data.key);

View File

@ -35,7 +35,7 @@ impl DatabaseUser for DatabaseUserImpl {
self self
.0 .0
.upgrade() .upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.user_id() .user_id()
} }
@ -43,7 +43,7 @@ impl DatabaseUser for DatabaseUserImpl {
self self
.0 .0
.upgrade() .upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.token() .token()
} }
@ -51,7 +51,7 @@ impl DatabaseUser for DatabaseUserImpl {
self self
.0 .0
.upgrade() .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) .get_collab_db(uid)
} }
} }

View File

@ -32,7 +32,7 @@ impl DocumentUser for DocumentUserImpl {
self self
.0 .0
.upgrade() .upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.user_id() .user_id()
} }
@ -40,7 +40,7 @@ impl DocumentUser for DocumentUserImpl {
self self
.0 .0
.upgrade() .upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.token() .token()
} }
@ -48,7 +48,7 @@ impl DocumentUser for DocumentUserImpl {
self self
.0 .0
.upgrade() .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) .get_collab_db(uid)
} }
} }

View File

@ -69,7 +69,7 @@ impl FolderUser for FolderUserImpl {
self self
.0 .0
.upgrade() .upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.user_id() .user_id()
} }
@ -77,7 +77,7 @@ impl FolderUser for FolderUserImpl {
self self
.0 .0
.upgrade() .upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.token() .token()
} }
@ -85,7 +85,7 @@ impl FolderUser for FolderUserImpl {
self self
.0 .0
.upgrade() .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) .get_collab_db(uid)
} }
} }
@ -305,7 +305,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
ViewLayout::Calendar => make_default_calendar(view_id, &name), ViewLayout::Calendar => make_default_calendar(view_id, &name),
ViewLayout::Document => { ViewLayout::Document => {
return FutureResult::new(async move { 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, _ => CSVFormat::Original,
}; };
FutureResult::new(async move { 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 database_manager
.import_csv(view_id, content, format) .import_csv(view_id, content, format)
.await?; .await?;
@ -359,7 +360,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
let database_layout = match new.layout { let database_layout = match new.layout {
ViewLayout::Document => { ViewLayout::Document => {
return FutureResult::new(async { 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, ViewLayout::Grid => DatabaseLayoutPB::Grid,

View File

@ -422,9 +422,9 @@ impl LocalServerDB for LocalServerDBImpl {
fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result<Vec<Vec<u8>>, FlowyError> { fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result<Vec<Vec<u8>>, FlowyError> {
let collab_db = open_collab_db(&self.storage_path, uid)?; let collab_db = open_collab_db(&self.storage_path, uid)?;
let read_txn = collab_db.read_txn(); let read_txn = collab_db.read_txn();
let updates = read_txn let updates = read_txn.get_all_updates(uid, object_id).map_err(|e| {
.get_all_updates(uid, object_id) FlowyError::internal().with_context(format!("Failed to open collab db: {:?}", e))
.map_err(|e| FlowyError::internal().context(format!("Failed to open collab db: {:?}", e)))?; })?;
Ok(updates) Ok(updates)
} }

View File

@ -22,7 +22,7 @@ fn upgrade_manager(
) -> FlowyResult<Arc<DatabaseManager>> { ) -> FlowyResult<Arc<DatabaseManager>> {
let manager = database_manager let manager = database_manager
.upgrade() .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) Ok(manager)
} }
@ -459,7 +459,7 @@ pub(crate) async fn create_row_handler(
.create_row(&view_id, group_id, params) .create_row(&view_id, group_id, params)
.await? .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)), Some(row) => data_result_ok(RowMetaPB::from(row.meta)),
} }
} }
@ -510,9 +510,10 @@ pub(crate) async fn new_select_option_handler(
.create_select_option(&params.field_id, params.option_name) .create_select_option(&params.field_id, params.option_name)
.await; .await;
match result { match result {
None => { None => Err(
Err(FlowyError::record_not_found().context("Create select option fail. Can't find the field")) FlowyError::record_not_found()
}, .with_context("Create select option fail. Can't find the field"),
),
Some(pb) => data_result_ok(pb), Some(pb) => data_result_ok(pb),
} }
} }

View File

@ -95,7 +95,7 @@ impl DatabaseManager {
collab_raw_data = updates; collab_raw_data = updates;
}, },
Err(err) => { Err(err) => {
return Err(FlowyError::record_not_found().context(format!( return Err(FlowyError::record_not_found().with_context(format!(
"get workspace database :{} failed: {}", "get workspace database :{} failed: {}",
database_storage_id, err, database_storage_id, err,
))); )));
@ -156,7 +156,7 @@ impl DatabaseManager {
let wdb = self.get_workspace_database().await?; let wdb = self.get_workspace_database().await?;
wdb.get_database_id_with_view_id(view_id).ok_or_else(|| { wdb.get_database_id_with_view_id(view_id).ok_or_else(|| {
FlowyError::record_not_found() 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<Arc<WorkspaceDatabase>> { async fn get_workspace_database(&self) -> FlowyResult<Arc<WorkspaceDatabase>> {
let database = self.workspace_database.read().await; let database = self.workspace_database.read().await;
match &*database { 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()), Some(user_database) => Ok(user_database.clone()),
} }
} }

View File

@ -32,7 +32,7 @@ impl TypeCellData {
pub fn from_json_str(s: &str) -> FlowyResult<Self> { pub fn from_json_str(s: &str) -> FlowyResult<Self> {
let type_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| { let type_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| {
let msg = format!("Deserialize {} to type cell data failed.{}", s, 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) Ok(type_cell_data)
} }

View File

@ -673,7 +673,7 @@ impl DatabaseEditor {
Some(field) => Ok(field), Some(field) => Ok(field),
None => { None => {
let msg = format!("Field with id:{} not found", &field_id); 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) (field, database.get_cell(field_id, &row_id).cell)
@ -767,7 +767,8 @@ impl DatabaseEditor {
.fields .fields
.get_field(field_id) .get_field(field_id)
.ok_or_else(|| { .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()); debug_assert!(FieldType::from(field.field_type).is_select_option());
@ -802,7 +803,7 @@ impl DatabaseEditor {
Some(field) => Ok(field), Some(field) => Ok(field),
None => { None => {
let msg = format!("Field with id:{} not found", &field_id); 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)?; let mut type_option = select_type_option_from_field(&field)?;
@ -868,7 +869,8 @@ impl DatabaseEditor {
.fields .fields
.get_field(field_id) .get_field(field_id)
.ok_or_else(|| { .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()); debug_assert!(FieldType::from(field.field_type).is_checklist());
@ -1047,11 +1049,10 @@ impl DatabaseEditor {
&self, &self,
view_id: &str, view_id: &str,
) -> FlowyResult<DatabaseViewSettingPB> { ) -> FlowyResult<DatabaseViewSettingPB> {
let view = self let view =
.database self.database.lock().get_view(view_id).ok_or_else(|| {
.lock() FlowyError::record_not_found().with_context("Can't find the database view")
.get_view(view_id) })?;
.ok_or_else(|| FlowyError::record_not_found().context("Can't find the database view"))?;
Ok(database_view_setting_pb_from_view(view)) Ok(database_view_setting_pb_from_view(view))
} }

View File

@ -418,7 +418,7 @@ impl DatabaseViewEditor {
.as_ref() .as_ref()
.and_then(|group| group.get_group(group_id)) .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)), Some((_, group)) => Ok(GroupPB::from(group)),
} }
} }

View File

@ -125,7 +125,10 @@ impl CellDataChangeset for RichTextTypeOption {
_cell: Option<Cell>, _cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> { ) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
if changeset.len() > 10000 { 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 { } else {
let text_cell_data = StrCellData(changeset); let text_cell_data = StrCellData(changeset);
Ok((text_cell_data.clone().into(), text_cell_data)) Ok((text_cell_data.clone().into(), text_cell_data))

View File

@ -223,7 +223,9 @@ where
})?; })?;
Ok(()) 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"),
),
} }
} }

View File

@ -33,7 +33,7 @@ impl CSVExport {
.collect::<Vec<String>>(); .collect::<Vec<String>>();
wtr wtr
.write_record(&field_records) .write_record(&field_records)
.map_err(|e| FlowyError::internal().context(e))?; .map_err(|e| FlowyError::internal().with_context(e))?;
// Write rows // Write rows
let mut field_by_field_id = IndexMap::new(); let mut field_by_field_id = IndexMap::new();
@ -63,8 +63,8 @@ impl CSVExport {
let data = wtr let data = wtr
.into_inner() .into_inner()
.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().context(e))?; let csv = String::from_utf8(data).map_err(|e| FlowyError::internal().with_context(e))?;
Ok(csv) Ok(csv)
} }
} }

View File

@ -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::database::{gen_database_id, gen_field_id, gen_row_id};
use collab_database::fields::Field; use collab_database::fields::Field;
use collab_database::rows::{new_cell_builder, Cell, CreateRowParams}; use collab_database::rows::{new_cell_builder, Cell, CreateRowParams};
use collab_database::views::{CreateDatabaseParams, DatabaseLayout}; use collab_database::views::{CreateDatabaseParams, DatabaseLayout};
use flowy_error::{FlowyError, FlowyResult}; 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)] #[derive(Default)]
pub struct CSVImporter; pub struct CSVImporter;
@ -41,7 +43,7 @@ impl CSVImporter {
fn get_fields_and_rows(&self, content: String) -> Result<FieldsRows, FlowyError> { fn get_fields_and_rows(&self, content: String) -> Result<FieldsRows, FlowyError> {
let mut fields: Vec<String> = vec![]; let mut fields: Vec<String> = vec![];
if content.is_empty() { 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()); let mut reader = csv::Reader::from_reader(content.as_bytes());
@ -50,7 +52,7 @@ impl CSVImporter {
fields.push(header.to_string()); fields.push(header.to_string());
} }
} else { } else {
return Err(FlowyError::invalid_data().context("Header not found")); return Err(FlowyError::invalid_data().with_context("Header not found"));
} }
let rows = reader let rows = reader
@ -164,9 +166,10 @@ pub struct ImportResult {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::services::share::csv::{CSVFormat, CSVImporter};
use collab_database::database::gen_database_view_id; use collab_database::database::gen_database_view_id;
use crate::services::share::csv::{CSVFormat, CSVImporter};
#[test] #[test]
fn test_import_csv_from_str() { fn test_import_csv_from_str() {
let s = r#"Name,Tags,Number,Date,Checkbox,URL let s = r#"Name,Tags,Number,Date,Checkbox,URL

View File

@ -22,7 +22,7 @@ fn upgrade_document(
) -> FlowyResult<Arc<DocumentManager>> { ) -> FlowyResult<Arc<DocumentManager>> {
let manager = document_manager let manager = document_manager
.upgrade() .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) Ok(manager)
} }

View File

@ -3,9 +3,12 @@ use thiserror::Error;
use flowy_derive::ProtoBuf_Enum; 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)] #[repr(u8)]
pub enum ErrorCode { pub enum ErrorCode {
#[default]
#[error("Internal error")] #[error("Internal error")]
Internal = 0, Internal = 0,
@ -226,6 +229,9 @@ pub enum ErrorCode {
#[error("It appears that the collaboration object's data has not been fully synchronized")] #[error("It appears that the collaboration object's data has not been fully synchronized")]
CollabDataNotSync = 75, CollabDataNotSync = 75,
#[error("It appears that the workspace data has not been fully synchronized")]
WorkspaceDataNotSync = 76,
} }
impl ErrorCode { impl ErrorCode {

View File

@ -1,6 +1,7 @@
use std::convert::TryInto;
use std::fmt::Debug; use std::fmt::Debug;
use anyhow::Result; use protobuf::ProtobufError;
use thiserror::Error; use thiserror::Error;
use flowy_derive::ProtoBuf; use flowy_derive::ProtoBuf;
@ -13,10 +14,13 @@ pub type FlowyResult<T> = anyhow::Result<T, FlowyError>;
#[error("{code:?}: {msg}")] #[error("{code:?}: {msg}")]
pub struct FlowyError { pub struct FlowyError {
#[pb(index = 1)] #[pb(index = 1)]
pub code: i32, pub code: ErrorCode,
#[pb(index = 2)] #[pb(index = 2)]
pub msg: String, pub msg: String,
#[pb(index = 3)]
pub payload: Vec<u8>,
} }
macro_rules! static_flowy_error { macro_rules! static_flowy_error {
@ -31,17 +35,23 @@ macro_rules! static_flowy_error {
impl FlowyError { impl FlowyError {
pub fn new<T: ToString>(code: ErrorCode, msg: T) -> Self { pub fn new<T: ToString>(code: ErrorCode, msg: T) -> Self {
Self { Self {
code: code.value(), code,
msg: msg.to_string(), msg: msg.to_string(),
payload: vec![],
} }
} }
pub fn context<T: Debug>(mut self, error: T) -> Self { pub fn with_context<T: Debug>(mut self, error: T) -> Self {
self.msg = format!("{:?}", error); self.msg = format!("{:?}", error);
self self
} }
pub fn with_payload<T: TryInto<Vec<u8>, Error = ProtobufError>>(mut self, payload: T) -> Self {
self.payload = payload.try_into().unwrap_or_default();
self
}
pub fn is_record_not_found(&self) -> bool { pub fn is_record_not_found(&self) -> bool {
self.code == ErrorCode::RecordNotFound.value() self.code == ErrorCode::RecordNotFound
} }
static_flowy_error!(internal, ErrorCode::Internal); static_flowy_error!(internal, ErrorCode::Internal);
@ -93,9 +103,11 @@ impl FlowyError {
impl std::convert::From<ErrorCode> for FlowyError { impl std::convert::From<ErrorCode> for FlowyError {
fn from(code: ErrorCode) -> Self { fn from(code: ErrorCode) -> Self {
let msg = format!("{}", code);
FlowyError { FlowyError {
code: code.value(), code,
msg: format!("{}", code), msg,
payload: vec![],
} }
} }
} }
@ -104,18 +116,18 @@ pub fn internal_error<T>(e: T) -> FlowyError
where where
T: std::fmt::Debug, T: std::fmt::Debug,
{ {
FlowyError::internal().context(e) FlowyError::internal().with_context(e)
} }
impl std::convert::From<std::io::Error> for FlowyError { impl std::convert::From<std::io::Error> for FlowyError {
fn from(error: std::io::Error) -> Self { fn from(error: std::io::Error) -> Self {
FlowyError::internal().context(error) FlowyError::internal().with_context(error)
} }
} }
impl std::convert::From<protobuf::ProtobufError> for FlowyError { impl std::convert::From<protobuf::ProtobufError> for FlowyError {
fn from(e: protobuf::ProtobufError) -> Self { fn from(e: protobuf::ProtobufError) -> Self {
FlowyError::internal().context(e) FlowyError::internal().with_context(e)
} }
} }

View File

@ -1,15 +1,16 @@
use crate::FlowyError;
use collab_database::error::DatabaseError; use collab_database::error::DatabaseError;
use collab_document::error::DocumentError; use collab_document::error::DocumentError;
use crate::FlowyError;
impl From<DatabaseError> for FlowyError { impl From<DatabaseError> for FlowyError {
fn from(error: DatabaseError) -> Self { fn from(error: DatabaseError) -> Self {
FlowyError::internal().context(error) FlowyError::internal().with_context(error)
} }
} }
impl From<DocumentError> for FlowyError { impl From<DocumentError> for FlowyError {
fn from(error: DocumentError) -> Self { fn from(error: DocumentError) -> Self {
FlowyError::internal().context(error) FlowyError::internal().with_context(error)
} }
} }

View File

@ -2,12 +2,12 @@ use crate::FlowyError;
impl std::convert::From<flowy_sqlite::Error> for FlowyError { impl std::convert::From<flowy_sqlite::Error> for FlowyError {
fn from(error: flowy_sqlite::Error) -> Self { fn from(error: flowy_sqlite::Error) -> Self {
FlowyError::internal().context(error) FlowyError::internal().with_context(error)
} }
} }
impl std::convert::From<::r2d2::Error> for FlowyError { impl std::convert::From<::r2d2::Error> for FlowyError {
fn from(error: r2d2::Error) -> Self { fn from(error: r2d2::Error) -> Self {
FlowyError::internal().context(error) FlowyError::internal().with_context(error)
} }
} }

View File

@ -1,7 +1,11 @@
use crate::FlowyError;
use bytes::Bytes;
use lib_dispatch::prelude::{AFPluginEventResponse, ResponseBuilder};
use std::convert::TryInto; use std::convert::TryInto;
use bytes::Bytes;
use lib_dispatch::prelude::{AFPluginEventResponse, ResponseBuilder};
use crate::FlowyError;
impl lib_dispatch::Error for FlowyError { impl lib_dispatch::Error for FlowyError {
fn as_response(&self) -> AFPluginEventResponse { fn as_response(&self) -> AFPluginEventResponse {
let bytes: Bytes = self.clone().try_into().unwrap(); let bytes: Bytes = self.clone().try_into().unwrap();

View File

@ -2,6 +2,6 @@ use crate::FlowyError;
impl std::convert::From<tokio_postgres::Error> for FlowyError { impl std::convert::From<tokio_postgres::Error> for FlowyError {
fn from(error: tokio_postgres::Error) -> Self { fn from(error: tokio_postgres::Error) -> Self {
FlowyError::internal().context(error) FlowyError::internal().with_context(error)
} }
} }

View File

@ -1,8 +1,9 @@
use crate::FlowyError;
use reqwest::Error; use reqwest::Error;
use crate::FlowyError;
impl std::convert::From<reqwest::Error> for FlowyError { impl std::convert::From<reqwest::Error> for FlowyError {
fn from(error: Error) -> Self { fn from(error: Error) -> Self {
FlowyError::http().context(error) FlowyError::http().with_context(error)
} }
} }

View File

@ -2,6 +2,6 @@ use crate::FlowyError;
impl std::convert::From<serde_json::Error> for FlowyError { impl std::convert::From<serde_json::Error> for FlowyError {
fn from(error: serde_json::Error) -> Self { fn from(error: serde_json::Error) -> Self {
FlowyError::serde().context(error) FlowyError::serde().with_context(error)
} }
} }

View File

@ -1,10 +1,10 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::FlowyError;
use crate::entities::parser::empty_str::NotEmptyStr; use crate::entities::parser::empty_str::NotEmptyStr;
use crate::entities::ViewLayoutPB; use crate::entities::ViewLayoutPB;
use crate::share::{ImportParams, ImportType}; use crate::share::{ImportParams, ImportType};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::FlowyError;
#[derive(Clone, Debug, ProtoBuf_Enum)] #[derive(Clone, Debug, ProtoBuf_Enum)]
pub enum ImportTypePB { pub enum ImportTypePB {
HistoryDocument = 0, HistoryDocument = 0,
@ -69,7 +69,7 @@ impl TryInto<ImportParams> for ImportPB {
None => None, None => None,
Some(file_path) => Some( Some(file_path) => Some(
NotEmptyStr::parse(file_path) 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, .0,
), ),
}; };

View File

@ -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::{ use crate::{
entities::parser::workspace::{WorkspaceDesc, WorkspaceIdentify, WorkspaceName}, entities::parser::workspace::{WorkspaceDesc, WorkspaceIdentify, WorkspaceName},
entities::view::ViewPB, 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)] #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)]
pub struct WorkspacePB { pub struct WorkspacePB {
@ -197,3 +200,12 @@ impl From<SyncState> for FolderSyncStatePB {
} }
} }
} }
#[derive(ProtoBuf, Default)]
pub struct UserFolderPB {
#[pb(index = 1)]
pub uid: i64,
#[pb(index = 2)]
pub workspace_id: String,
}

View File

@ -12,7 +12,7 @@ fn upgrade_folder(
) -> FlowyResult<Arc<FolderManager>> { ) -> FlowyResult<Arc<FolderManager>> {
let folder = folder_manager let folder = folder_manager
.upgrade() .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) Ok(folder)
} }
@ -45,10 +45,10 @@ pub(crate) async fn open_workspace_handler(
let folder = upgrade_folder(folder)?; let folder = upgrade_folder(folder)?;
let params: WorkspaceIdPB = data.into_inner(); let params: WorkspaceIdPB = data.into_inner();
match params.value { 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) => { Some(workspace_id) => {
if workspace_id.is_empty() { 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 { } else {
let workspace = folder.open_workspace(&workspace_id).await?; let workspace = folder.open_workspace(&workspace_id).await?;
let views = folder.get_workspace_views(&workspace_id).await?; let views = folder.get_workspace_views(&workspace_id).await?;

View File

@ -134,7 +134,7 @@ pub enum FolderEvent {
#[event(input = "ImportPB")] #[event(input = "ImportPB")]
ImportData = 30, ImportData = 30,
#[event()] #[event(input = "WorkspaceIdPB", output = "RepeatedFolderSnapshotPB")]
GetFolderSnapshots = 31, GetFolderSnapshots = 31,
/// Moves a nested view to a new location in the hierarchy. /// Moves a nested view to a new location in the hierarchy.
/// ///

View File

@ -10,7 +10,7 @@ use collab_folder::core::{
FavoritesInfo, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, FavoritesInfo, Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo,
View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace, View, ViewChange, ViewChangeReceiver, ViewLayout, ViewUpdate, Workspace,
}; };
use parking_lot::Mutex; use parking_lot::{Mutex, RwLock};
use tokio_stream::wrappers::WatchStream; use tokio_stream::wrappers::WatchStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tracing::{event, Level}; use tracing::{event, Level};
@ -22,7 +22,8 @@ use crate::entities::icon::UpdateViewIconParams;
use crate::entities::{ use crate::entities::{
view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams, view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams,
CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB, CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB,
RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, ViewPB, WorkspacePB, RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, UserFolderPB, ViewPB,
WorkspacePB,
}; };
use crate::notification::{ use crate::notification::{
send_notification, send_workspace_notification, send_workspace_setting_notification, send_notification, send_workspace_notification, send_workspace_setting_notification,
@ -42,6 +43,7 @@ pub trait FolderUser: Send + Sync {
} }
pub struct FolderManager { pub struct FolderManager {
workspace_id: RwLock<Option<String>>,
mutex_folder: Arc<MutexFolder>, mutex_folder: Arc<MutexFolder>,
collab_builder: Arc<AppFlowyCollabBuilder>, collab_builder: Arc<AppFlowyCollabBuilder>,
user: Arc<dyn FolderUser>, user: Arc<dyn FolderUser>,
@ -66,6 +68,7 @@ impl FolderManager {
collab_builder, collab_builder,
operation_handlers, operation_handlers,
cloud_service, cloud_service,
workspace_id: Default::default(),
}; };
Ok(manager) Ok(manager)
@ -73,7 +76,14 @@ impl FolderManager {
pub async fn get_current_workspace(&self) -> FlowyResult<WorkspacePB> { pub async fn get_current_workspace(&self) -> FlowyResult<WorkspacePB> {
self.with_folder( 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| { |folder| {
let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| {
let views = get_workspace_view_pbs(&workspace.id, folder); let views = get_workspace_view_pbs(&workspace.id, folder);
@ -87,7 +97,7 @@ impl FolderManager {
// from the folder. Otherwise, return an error. // from the folder. Otherwise, return an error.
let mut workspaces = folder.workspaces.get_all_workspaces(); let mut workspaces = folder.workspaces.get_all_workspaces();
if workspaces.is_empty() { 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 { } else {
tracing::error!("Can't find the current workspace, use the first workspace"); tracing::error!("Can't find the current workspace, use the first workspace");
let workspace = workspaces.remove(0); let workspace = workspaces.remove(0);
@ -119,9 +129,10 @@ impl FolderManager {
} }
pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult<Vec<ViewPB>> { pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult<Vec<ViewPB>> {
let views = self.with_folder(vec![], |folder| { let views = self.with_folder(
get_workspace_view_pbs(workspace_id, folder) || vec![],
}); |folder| get_workspace_view_pbs(workspace_id, folder),
);
Ok(views) Ok(views)
} }
@ -134,6 +145,7 @@ impl FolderManager {
workspace_id: &str, workspace_id: &str,
initial_data: FolderInitializeData, initial_data: FolderInitializeData,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
*self.workspace_id.write() = Some(workspace_id.to_string());
let workspace_id = workspace_id.to_string(); let workspace_id = workspace_id.to_string();
if let Ok(collab_db) = self.user.collab_db(uid) { if let Ok(collab_db) = self.user.collab_db(uid) {
let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); let (view_tx, view_rx) = tokio::sync::broadcast::channel(100);
@ -157,10 +169,7 @@ impl FolderManager {
}, },
FolderInitializeData::Raw(raw_data) => { FolderInitializeData::Raw(raw_data) => {
if raw_data.is_empty() { if raw_data.is_empty() {
return Err(FlowyError::new( return Err(workspace_data_not_sync_error(uid, &workspace_id));
ErrorCode::CollabDataNotSync,
"Can't fetch the workspace from server",
));
} }
let collab = self.collab_for_folder(uid, &workspace_id, collab_db, raw_data)?; let collab = self.collab_for_folder(uid, &workspace_id, collab_db, raw_data)?;
Folder::open(collab, Some(folder_notifier)) Folder::open(collab, Some(folder_notifier))
@ -299,10 +308,13 @@ impl FolderManager {
.create_workspace(self.user.user_id()?, &params.name) .create_workspace(self.user.user_id()?, &params.name)
.await?; .await?;
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.workspaces.create_workspace(workspace.clone()); folder.workspaces.create_workspace(workspace.clone());
folder.set_current_workspace(&workspace.id); folder.set_current_workspace(&workspace.id);
}); },
);
let repeated_workspace = RepeatedWorkspacePB { let repeated_workspace = RepeatedWorkspacePB {
items: vec![workspace.clone().into()], items: vec![workspace.clone().into()],
@ -313,20 +325,26 @@ impl FolderManager {
#[tracing::instrument(level = "info", skip_all, err)] #[tracing::instrument(level = "info", skip_all, err)]
pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult<Workspace> { pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult<Workspace> {
self.with_folder(Err(FlowyError::internal()), |folder| { self.with_folder(
|| Err(FlowyError::internal()),
|folder| {
let workspace = folder let workspace = folder
.workspaces .workspaces
.get_workspace(workspace_id) .get_workspace(workspace_id)
.ok_or_else(|| { .ok_or_else(|| {
FlowyError::record_not_found().context("Can't open not existing workspace") FlowyError::record_not_found().with_context("Can't open not existing workspace")
})?; })?;
folder.set_current_workspace(&workspace.id); folder.set_current_workspace(&workspace.id);
Ok::<Workspace, FlowyError>(workspace) Ok::<Workspace, FlowyError>(workspace)
}) },
)
} }
pub async fn get_workspace(&self, workspace_id: &str) -> Option<Workspace> { pub async fn get_workspace(&self, workspace_id: &str) -> Option<Workspace> {
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<String> { async fn get_current_workspace_id(&self) -> FlowyResult<String> {
@ -335,22 +353,30 @@ impl FolderManager {
.lock() .lock()
.as_ref() .as_ref()
.and_then(|folder| folder.get_current_workspace_id()) .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<F, Output>(&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<F1, F2, Output>(&self, none_callback: F1, f2: F2) -> Output
where where
F: FnOnce(&Folder) -> Output, F1: FnOnce() -> Output,
F2: FnOnce(&Folder) -> Output,
{ {
let folder = self.mutex_folder.lock(); let folder = self.mutex_folder.lock();
match &*folder { match &*folder {
None => default_value, None => none_callback(),
Some(folder) => f(folder), Some(folder) => f2(folder),
} }
} }
pub async fn get_all_workspaces(&self) -> Vec<Workspace> { pub async fn get_all_workspaces(&self) -> Vec<Workspace> {
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<View> { pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult<View> {
@ -381,9 +407,12 @@ impl FolderManager {
let index = params.index; let index = params.index;
let view = create_view(params, view_layout); let view = create_view(params, view_layout);
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.insert_view(view.clone(), index); folder.insert_view(view.clone(), index);
}); },
);
Ok(view) Ok(view)
} }
@ -402,15 +431,18 @@ impl FolderManager {
.create_built_in_view(user_id, &params.view_id, &params.name, view_layout.clone()) .create_built_in_view(user_id, &params.view_id, &params.name, view_layout.clone())
.await?; .await?;
let view = create_view(params, view_layout); let view = create_view(params, view_layout);
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.insert_view(view.clone(), None); folder.insert_view(view.clone(), None);
}); },
);
Ok(view) Ok(view)
} }
#[tracing::instrument(level = "debug", skip(self), err)] #[tracing::instrument(level = "debug", skip(self), err)]
pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { 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)?; let handler = self.get_handler(&view.layout)?;
handler.close_view(view_id).await?; handler.close_view(view_id).await?;
} }
@ -455,7 +487,9 @@ 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()` /// 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)] #[tracing::instrument(level = "debug", skip(self), err)]
pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> { pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> {
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
if let Some(view) = folder.views.get_view(view_id) { if let Some(view) = folder.views.get_view(view_id) {
self.unfavorite_view_and_decendants(view.clone(), folder); self.unfavorite_view_and_decendants(view.clone(), folder);
folder.add_trash(vec![view_id.to_string()]); folder.add_trash(vec![view_id.to_string()]);
@ -472,7 +506,8 @@ impl FolderManager {
ChildViewChangeReason::DidDeleteView, ChildViewChangeReason::DidDeleteView,
); );
} }
}); },
);
Ok(()) Ok(())
} }
@ -528,9 +563,12 @@ impl FolderManager {
) -> FlowyResult<()> { ) -> FlowyResult<()> {
let view = self.get_view(&view_id).await?; let view = self.get_view(&view_id).await?;
let old_parent_id = view.parent_view_id; let old_parent_id = view.parent_view_id;
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); folder.move_nested_view(&view_id, &new_parent_id, prev_view_id);
}); },
);
notify_parent_view_did_change( notify_parent_view_did_change(
self.mutex_folder.clone(), self.mutex_folder.clone(),
vec![new_parent_id, old_parent_id], vec![new_parent_id, old_parent_id],
@ -574,9 +612,12 @@ impl FolderManager {
if let (Some(actual_from_index), Some(actual_to_index)) = if let (Some(actual_from_index), Some(actual_to_index)) =
(actual_from_index, actual_to_index) (actual_from_index, actual_to_index)
{ {
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); 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]); 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. /// Return a list of views that belong to the given parent view id.
#[tracing::instrument(level = "debug", skip(self, parent_view_id), err)] #[tracing::instrument(level = "debug", skip(self, parent_view_id), err)]
pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult<Vec<Arc<View>>> { pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult<Vec<Arc<View>>> {
let views = self.with_folder(vec![], |folder| { let views = self.with_folder(
folder.views.get_views_belong_to(parent_view_id) || vec![],
}); |folder| folder.views.get_views_belong_to(parent_view_id),
);
Ok(views) Ok(views)
} }
@ -625,8 +667,8 @@ impl FolderManager {
#[tracing::instrument(level = "debug", skip(self), err)] #[tracing::instrument(level = "debug", skip(self), err)]
pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> { pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> {
let view = self let view = self
.with_folder(None, |folder| folder.views.get_view(view_id)) .with_folder(|| None, |folder| folder.views.get_view(view_id))
.ok_or_else(|| FlowyError::record_not_found().context("Can't duplicate the view"))?; .ok_or_else(|| FlowyError::record_not_found().with_context("Can't duplicate the view"))?;
let handler = self.get_handler(&view.layout)?; let handler = self.get_handler(&view.layout)?;
let view_data = handler.duplicate_view(&view.id).await?; let view_data = handler.duplicate_view(&view.id).await?;
@ -670,14 +712,16 @@ impl FolderManager {
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_current_view(&self) -> Option<ViewPB> { pub(crate) async fn get_current_view(&self) -> Option<ViewPB> {
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() 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. /// 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)] #[tracing::instrument(level = "debug", skip(self), err)]
pub async fn toggle_favorites(&self, view_id: &str) -> FlowyResult<()> { pub async fn toggle_favorites(&self, view_id: &str) -> FlowyResult<()> {
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
if let Some(old_view) = folder.views.get_view(view_id) { if let Some(old_view) = folder.views.get_view(view_id) {
if old_view.is_favorite { if old_view.is_favorite {
folder.delete_favorites(vec![view_id.to_string()]); folder.delete_favorites(vec![view_id.to_string()]);
@ -685,7 +729,8 @@ impl FolderManager {
folder.add_favorites(vec![view_id.to_string()]); folder.add_favorites(vec![view_id.to_string()]);
} }
} }
}); },
);
self.send_toggle_favorite_notification(view_id).await; self.send_toggle_favorite_notification(view_id).await;
Ok(()) Ok(())
} }
@ -712,7 +757,9 @@ impl FolderManager {
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_all_favorites(&self) -> Vec<FavoritesInfo> { pub(crate) async fn get_all_favorites(&self) -> Vec<FavoritesInfo> {
self.with_folder(vec![], |folder| { self.with_folder(
|| vec![],
|folder| {
let trash_ids = folder let trash_ids = folder
.get_all_trash() .get_all_trash()
.into_iter() .into_iter()
@ -722,19 +769,23 @@ impl FolderManager {
let mut views = folder.get_all_favorites(); let mut views = folder.get_all_favorites();
views.retain(|view| !trash_ids.contains(&view.id)); views.retain(|view| !trash_ids.contains(&view.id));
views views
}) },
)
} }
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn get_all_trash(&self) -> Vec<TrashInfo> { pub(crate) async fn get_all_trash(&self) -> Vec<TrashInfo> {
self.with_folder(vec![], |folder| folder.get_all_trash()) self.with_folder(|| vec![], |folder| folder.get_all_trash())
} }
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn restore_all_trash(&self) { pub(crate) async fn restore_all_trash(&self) {
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.remote_all_trash(); folder.remote_all_trash();
}); },
);
send_notification("trash", FolderNotification::DidUpdateTrash) send_notification("trash", FolderNotification::DidUpdateTrash)
.payload(RepeatedTrashPB { items: vec![] }) .payload(RepeatedTrashPB { items: vec![] })
.send(); .send();
@ -742,15 +793,18 @@ impl FolderManager {
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn restore_trash(&self, trash_id: &str) { pub(crate) async fn restore_trash(&self, trash_id: &str) {
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.delete_trash(vec![trash_id.to_string()]); folder.delete_trash(vec![trash_id.to_string()]);
}); },
);
} }
/// Delete all the trash permanently. /// Delete all the trash permanently.
#[tracing::instrument(level = "trace", skip(self))] #[tracing::instrument(level = "trace", skip(self))]
pub(crate) async fn delete_all_trash(&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 { for trash in deleted_trash {
let _ = self.delete_trash(&trash.id).await; 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. /// is a database view. Then the database will be deleted as well.
#[tracing::instrument(level = "debug", skip(self, view_id), err)] #[tracing::instrument(level = "debug", skip(self, view_id), err)]
pub async fn delete_trash(&self, view_id: &str) -> FlowyResult<()> { pub async fn delete_trash(&self, view_id: &str) -> FlowyResult<()> {
let view = self.with_folder(None, |folder| folder.views.get_view(view_id)); let view = self.with_folder(|| None, |folder| folder.views.get_view(view_id));
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.delete_trash(vec![view_id.to_string()]); folder.delete_trash(vec![view_id.to_string()]);
folder.views.delete_views(vec![view_id]); folder.views.delete_views(vec![view_id]);
}); },
);
if let Some(view) = view { if let Some(view) = view {
if let Ok(handler) = self.get_handler(&view.layout) { if let Ok(handler) = self.get_handler(&view.layout) {
handler.delete_view(view_id).await?; handler.delete_view(view_id).await?;
@ -819,9 +876,12 @@ impl FolderManager {
}; };
let view = create_view(params, import_data.view_layout); let view = create_view(params, import_data.view_layout);
self.with_folder((), |folder| { self.with_folder(
|| (),
|folder| {
folder.insert_view(view.clone(), None); folder.insert_view(view.clone(), None);
}); },
);
notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]); notify_parent_view_did_change(self.mutex_folder.clone(), vec![view.parent_view_id.clone()]);
Ok(view) Ok(view)
} }
@ -831,12 +891,15 @@ impl FolderManager {
where where
F: FnOnce(ViewUpdate) -> Option<View>, F: FnOnce(ViewUpdate) -> Option<View>,
{ {
let value = self.with_folder(None, |folder| { let value = self.with_folder(
|| None,
|folder| {
let old_view = folder.views.get_view(view_id); let old_view = folder.views.get_view(view_id);
let new_view = folder.views.update_view(view_id, f); 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 Some((Some(old_view), Some(new_view))) = value {
if let Ok(handler) = self.get_handler(&old_view.layout) { if let Ok(handler) = self.get_handler(&old_view.layout) {
@ -858,7 +921,7 @@ impl FolderManager {
view_layout: &ViewLayout, view_layout: &ViewLayout,
) -> FlowyResult<Arc<dyn FolderOperationHandler + Send + Sync>> { ) -> FlowyResult<Arc<dyn FolderOperationHandler + Send + Sync>> {
match self.operation_handlers.get(view_layout) { 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: {:?}", "Get data processor failed. Unknown layout type: {:?}",
view_layout view_layout
))), ))),
@ -871,7 +934,9 @@ impl FolderManager {
/// Otherwise, the parent_view_id is the parent view id of the view. The child_view_ids is the /// 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. /// child view ids of the view.
async fn get_view_relation(&self, view_id: &str) -> Option<(bool, String, Vec<String>)> { async fn get_view_relation(&self, view_id: &str) -> Option<(bool, String, Vec<String>)> {
self.with_folder(None, |folder| { self.with_folder(
|| None,
|folder| {
let view = folder.views.get_view(view_id)?; let view = folder.views.get_view(view_id)?;
match folder.views.get_view(&view.parent_view_id) { match folder.views.get_view(&view.parent_view_id) {
None => folder.get_current_workspace().map(|workspace| { None => folder.get_current_workspace().map(|workspace| {
@ -898,7 +963,8 @@ impl FolderManager {
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
)), )),
} }
}) },
)
} }
pub async fn get_folder_snapshots( 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 { fn folder_not_init_error() -> FlowyError {
FlowyError::internal().context("Folder not initialized") FlowyError::internal().with_context("Folder not initialized")
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -1190,3 +1256,10 @@ fn is_exist_in_local_disk(user: &Arc<dyn FolderUser>, doc_id: &str) -> FlowyResu
Ok(false) 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(),
})
}

View File

@ -118,6 +118,10 @@ impl UserService for LocalServerUserAuthServiceImpl {
FutureResult::new(async { Ok(vec![]) }) FutureResult::new(async { Ok(vec![]) })
} }
fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> {
FutureResult::new(async { Ok(()) })
}
fn create_collab_object( fn create_collab_object(
&self, &self,
_collab_object: &CollabObject, _collab_object: &CollabObject,

View File

@ -159,7 +159,7 @@ impl HttpRequestBuilder {
fn unexpected_empty_payload(url: &str) -> FlowyError { fn unexpected_empty_payload(url: &str) -> FlowyError {
let msg = format!("Request: {} receives unexpected empty payload", url); 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<HttpResponse, FlowyError> { async fn flowy_response_from(original: Response) -> Result<HttpResponse, FlowyError> {
@ -178,7 +178,7 @@ async fn get_response_data(original: Response) -> Result<Bytes, FlowyError> {
Some(error) => Err(FlowyError::new(error.code, &error.msg)), Some(error) => Err(FlowyError::new(error.code, &error.msg)),
} }
} else { } else {
Err(FlowyError::http().context(original)) Err(FlowyError::http().with_context(original))
} }
} }

View File

@ -129,6 +129,11 @@ impl UserService for SelfHostedUserAuthServiceImpl {
FutureResult::new(async { Ok(vec![]) }) 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( fn create_collab_object(
&self, &self,
_collab_object: &CollabObject, _collab_object: &CollabObject,

View File

@ -161,8 +161,7 @@ where
.get_workspace_id() .get_workspace_id()
.ok_or(anyhow::anyhow!("Invalid workspace id"))?; .ok_or(anyhow::anyhow!("Invalid workspace id"))?;
let update_items = let update_items = get_updates_from_server(&object.object_id, &object.ty, &postgrest).await?;
get_updates_from_server(&object.object_id, &object.ty, postgrest.clone()).await?;
// If the update_items is empty, we can send the init_update directly // If the update_items is empty, we can send the init_update directly
if update_items.is_empty() { if update_items.is_empty() {
@ -175,14 +174,42 @@ where
) )
.await?; .await?;
} else { } else {
flush_collab_with_update(object, update_items, &postgrest, init_update, self.secret())
.await?;
}
Ok(())
}
fn subscribe_remote_updates(&self, _object: &CollabObject) -> Option<RemoteUpdateReceiver> {
let rx = self.rx.lock().take();
if rx.is_none() {
tracing::warn!("The receiver is already taken");
}
rx
}
}
pub(crate) async fn flush_collab_with_update(
object: &CollabObject,
update_items: Vec<UpdateItem>,
postgrest: &Arc<PostgresWrapper>,
update: Vec<u8>,
secret: Option<String>,
) -> Result<(), Error> {
// 2.Merge the updates into one and then delete the merged updates // 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??; let merge_result = spawn_blocking(move || merge_updates(update_items, update)).await??;
tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len()); 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 value_size = merge_result.new_update.len() as i32;
let md5 = md5(&merge_result.new_update); let md5 = md5(&merge_result.new_update);
tracing::trace!("Flush collab id:{} type:{}", object.object_id, object.ty);
let (new_update, encrypt) = let (new_update, encrypt) =
SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &self.secret())?; SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &secret)?;
let params = InsertParamsBuilder::new() let params = InsertParamsBuilder::new()
.insert("oid", object.object_id.clone()) .insert("oid", object.object_id.clone())
.insert("new_value", new_update) .insert("new_value", new_update)
@ -202,17 +229,7 @@ where
.await? .await?
.success() .success()
.await?; .await?;
}
Ok(()) Ok(())
}
fn subscribe_remote_updates(&self, _object: &CollabObject) -> Option<RemoteUpdateReceiver> {
let rx = self.rx.lock().take();
if rx.is_none() {
tracing::warn!("The receiver is already taken");
}
rx
}
} }
pub(crate) async fn send_update( pub(crate) async fn send_update(

View File

@ -72,7 +72,7 @@ where
let workspace_id = workspace_id.to_string(); let workspace_id = workspace_id.to_string();
FutureResult::new(async move { FutureResult::new(async move {
let postgrest = try_get_postgrest?; 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 let updates = updates
.into_iter() .into_iter()
.map(|item| item.value) .map(|item| item.value)

View File

@ -66,12 +66,14 @@ impl Action for FetchObjectUpdateAction {
Box::pin(async move { Box::pin(async move {
match weak_postgres.upgrade() { match weak_postgres.upgrade() {
None => Ok(vec![]), None => Ok(vec![]),
Some(postgrest) => match get_updates_from_server(&object_id, &object_ty, postgrest).await { Some(postgrest) => {
match get_updates_from_server(&object_id, &object_ty, &postgrest).await {
Ok(items) => Ok(items.into_iter().map(|item| item.value).collect()), Ok(items) => Ok(items.into_iter().map(|item| item.value).collect()),
Err(err) => { Err(err) => {
tracing::error!("Get {} updates failed with error: {:?}", object_id, err); tracing::error!("Get {} updates failed with error: {:?}", object_id, err);
Err(err) Err(err)
}, },
}
}, },
} }
}) })
@ -285,7 +287,7 @@ pub async fn batch_get_updates_from_server(
pub async fn get_updates_from_server( pub async fn get_updates_from_server(
object_id: &str, object_id: &str,
object_ty: &CollabType, object_ty: &CollabType,
postgrest: Arc<PostgresWrapper>, postgrest: &Arc<PostgresWrapper>,
) -> Result<Vec<UpdateItem>, Error> { ) -> Result<Vec<UpdateItem>, Error> {
let json = postgrest let json = postgrest
.from(table_name(object_ty)) .from(table_name(object_ty))

View File

@ -2,23 +2,29 @@ use std::str::FromStr;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use anyhow::Error; use anyhow::Error;
use collab::core::collab::MutexCollab;
use collab::core::origin::CollabOrigin;
use collab_plugins::cloud_storage::CollabObject; use collab_plugins::cloud_storage::CollabObject;
use parking_lot::RwLock; use parking_lot::RwLock;
use serde_json::Value; use serde_json::Value;
use tokio::sync::oneshot::channel; use tokio::sync::oneshot::channel;
use uuid::Uuid; use uuid::Uuid;
use flowy_folder_deps::cloud::{Folder, Workspace};
use flowy_user_deps::cloud::*; use flowy_user_deps::cloud::*;
use flowy_user_deps::entities::*; use flowy_user_deps::entities::*;
use flowy_user_deps::DEFAULT_USER_NAME; use flowy_user_deps::DEFAULT_USER_NAME;
use lib_infra::box_any::BoxAny; use lib_infra::box_any::BoxAny;
use lib_infra::future::FutureResult; 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::{ use crate::supabase::api::util::{
ExtendedResponse, InsertParamsBuilder, RealtimeBinaryColumnDecoder, SupabaseBinaryColumnDecoder, 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::define::*;
use crate::supabase::entities::UserProfileResponse; use crate::supabase::entities::UserProfileResponse;
use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent}; use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent};
@ -266,6 +272,39 @@ where
self.user_update_tx.as_ref().map(|tx| tx.subscribe()) 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( fn create_collab_object(
&self, &self,
collab_object: &CollabObject, collab_object: &CollabObject,
@ -516,3 +555,21 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler {
} }
} }
} }
fn empty_workspace_update(collab_object: &CollabObject) -> Vec<u8> {
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
}

View File

@ -127,7 +127,7 @@ async fn delete_view_event_test() {
.await .await
.error() .error()
.unwrap(); .unwrap();
assert_eq!(error.code, ErrorCode::RecordNotFound.value()); assert_eq!(error.code, ErrorCode::RecordNotFound);
} }
#[tokio::test] #[tokio::test]
@ -150,7 +150,7 @@ async fn put_back_trash_event_test() {
.await .await
.error() .error()
.unwrap(); .unwrap();
assert_eq!(error.code, ErrorCode::RecordNotFound.value()); assert_eq!(error.code, ErrorCode::RecordNotFound);
let payload = TrashIdPB { let payload = TrashIdPB {
id: view.id.clone(), id: view.id.clone(),
@ -480,7 +480,7 @@ async fn create_parent_view_with_invalid_name() {
.error() .error()
.unwrap() .unwrap()
.code, .code,
code.value() code
) )
} }
} }

View File

@ -27,7 +27,7 @@ async fn sign_up_with_invalid_email() {
.error() .error()
.unwrap() .unwrap()
.code, .code,
ErrorCode::EmailFormatInvalid.value() ErrorCode::EmailFormatInvalid
); );
} }
} }
@ -51,7 +51,7 @@ async fn sign_up_with_long_password() {
.error() .error()
.unwrap() .unwrap()
.code, .code,
ErrorCode::PasswordTooLong.value() ErrorCode::PasswordTooLong
); );
} }
@ -76,7 +76,7 @@ async fn sign_in_with_invalid_email() {
.error() .error()
.unwrap() .unwrap()
.code, .code,
ErrorCode::EmailFormatInvalid.value() ErrorCode::EmailFormatInvalid
); );
} }
} }

View File

@ -79,7 +79,7 @@ async fn user_update_with_invalid_email() {
.error() .error()
.unwrap() .unwrap()
.code, .code,
ErrorCode::EmailFormatInvalid.value() ErrorCode::EmailFormatInvalid
); );
} }
} }

View File

@ -103,7 +103,7 @@ async fn third_party_sign_up_with_duplicated_email() {
.await .await
.err() .err()
.unwrap(); .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()) .check_user_with_uuid(&uuid::Uuid::new_v4().to_string())
.await .await
.unwrap_err(); .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 .await
.unwrap(); .unwrap();
assert_eq!(error.code, ErrorCode::Conflict.value()); assert_eq!(error.code, ErrorCode::Conflict);
} }
} }

View File

@ -110,6 +110,8 @@ pub trait UserService: Send + Sync {
None None
} }
fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error>;
fn create_collab_object( fn create_collab_object(
&self, &self,
collab_object: &CollabObject, collab_object: &CollabObject,

View File

@ -48,6 +48,9 @@ pub struct UserProfilePB {
#[pb(index = 9)] #[pb(index = 9)]
pub encryption_type: EncryptionTypePB, pub encryption_type: EncryptionTypePB,
#[pb(index = 10)]
pub workspace_id: String,
} }
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
@ -78,6 +81,7 @@ impl std::convert::From<UserProfile> for UserProfilePB {
auth_type: user_profile.auth_type.into(), auth_type: user_profile.auth_type.into(),
encryption_sign, encryption_sign,
encryption_type: encryption_ty, encryption_type: encryption_ty,
workspace_id: user_profile.workspace_id,
} }
} }
} }
@ -274,3 +278,12 @@ impl From<HistoricalUser> for HistoricalUserPB {
} }
} }
} }
#[derive(ProtoBuf, Default, Clone)]
pub struct ResetWorkspacePB {
#[pb(index = 1)]
pub uid: i64,
#[pb(index = 2)]
pub workspace_id: String,
}

View File

@ -3,7 +3,7 @@ use std::{convert::TryInto, sync::Arc};
use serde_json::Value; use serde_json::Value;
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_sqlite::kv::StorePreferences; use flowy_sqlite::kv::StorePreferences;
use flowy_user_deps::cloud::UserCloudConfig; use flowy_user_deps::cloud::UserCloudConfig;
use flowy_user_deps::entities::*; use flowy_user_deps::entities::*;
@ -20,7 +20,7 @@ use crate::services::cloud_config::{
fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc<UserManager>> { fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc<UserManager>> {
let manager = manager let manager = manager
.upgrade() .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) Ok(manager)
} }
@ -29,7 +29,7 @@ fn upgrade_store_preferences(
) -> FlowyResult<Arc<StorePreferences>> { ) -> FlowyResult<Arc<StorePreferences>> {
let store = store let store = store
.upgrade() .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) Ok(store)
} }
@ -96,7 +96,15 @@ pub async fn get_user_profile_handler(
let manager = upgrade_manager(manager)?; let manager = upgrade_manager(manager)?;
let uid = manager.get_session()?.user_id; let uid = manager.get_session()?.user_id;
let user_profile = manager.get_user_profile(uid).await?; 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()) data_result_ok(user_profile.into())
} }
@ -250,7 +258,7 @@ pub async fn set_cloud_config_handler(
let update = data.into_inner(); let update = data.into_inner();
let store_preferences = upgrade_store_preferences(store_preferences)?; let store_preferences = upgrade_store_preferences(store_preferences)?;
let mut config = get_cloud_config(session.user_id, &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 { if let Some(enable_sync) = update.enable_sync {
manager.cloud_services.set_enable_sync(enable_sync); manager.cloud_services.set_enable_sync(enable_sync);
@ -429,3 +437,20 @@ pub async fn get_all_reminder_event_handler(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
data_result_ok(reminders.into()) data_result_ok(reminders.into())
} }
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn reset_workspace_handler(
data: AFPluginData<ResetWorkspacePB>,
manager: AFPluginState<Weak<UserManager>>,
) -> 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(())
}

View File

@ -54,6 +54,7 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin {
.event(UserEvent::PushRealtimeEvent, push_realtime_event_handler) .event(UserEvent::PushRealtimeEvent, push_realtime_event_handler)
.event(UserEvent::CreateReminder, create_reminder_event_handler) .event(UserEvent::CreateReminder, create_reminder_event_handler)
.event(UserEvent::GetAllReminders, get_all_reminder_event_handler) .event(UserEvent::GetAllReminders, get_all_reminder_event_handler)
.event(UserEvent::ResetWorkspace, reset_workspace_handler)
} }
pub struct SignUpContext { pub struct SignUpContext {
@ -271,4 +272,7 @@ pub enum UserEvent {
#[event(output = "RepeatedReminderPB")] #[event(output = "RepeatedReminderPB")]
GetAllReminders = 29, GetAllReminders = 29,
#[event(input = "ResetWorkspacePB")]
ResetWorkspace = 30,
} }

View File

@ -70,7 +70,7 @@ pub fn open_user_db(root: &str, user_id: i64) -> Result<Arc<ConnectionPool>, Flo
let dir = user_db_path_from_uid(root, user_id); let dir = user_db_path_from_uid(root, user_id);
tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir); tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir);
let db = flowy_sqlite::init(&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(); let pool = db.get_pool();
write_guard.insert(user_id.to_owned(), db); write_guard.insert(user_id.to_owned(), db);
drop(write_guard); drop(write_guard);

View File

@ -1,12 +1,14 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use std::sync::Arc; use std::sync::Arc;
use appflowy_integrate::{CollabObject, CollabType};
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::schema::user_workspace_table;
use flowy_sqlite::{query_dsl::*, ConnectionPool, ExpressionMethods}; use flowy_sqlite::{query_dsl::*, ConnectionPool, ExpressionMethods};
use flowy_user_deps::entities::UserWorkspace; use flowy_user_deps::entities::UserWorkspace;
use crate::entities::RepeatedUserWorkspacePB; use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB};
use crate::manager::UserManager; use crate::manager::UserManager;
use crate::notification::{send_notification, UserNotification}; use crate::notification::{send_notification, UserNotification};
use crate::services::user_workspace_sql::UserWorkspaceTable; use crate::services::user_workspace_sql::UserWorkspaceTable;
@ -84,6 +86,20 @@ impl UserManager {
} }
Ok(rows.into_iter().map(UserWorkspace::from).collect()) 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( pub fn save_user_workspaces(

View File

@ -21,10 +21,10 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
fn try_from(value: (i64, &UserWorkspace)) -> Result<Self, Self::Error> { fn try_from(value: (i64, &UserWorkspace)) -> Result<Self, Self::Error> {
if value.1.id.is_empty() { 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() { 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 { Ok(Self {

View File

@ -28,6 +28,16 @@ pub fn make_se_token_stream(ast_result: &ASTResult, ast: &ASTContainer) -> Optio
} }
} }
impl std::convert::TryInto<Vec<u8>> for #struct_ident {
type Error = ::protobuf::ProtobufError;
fn try_into(self) -> Result<Vec<u8>, 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 { impl std::convert::From<#struct_ident> for crate::protobuf::#pb_ty {
fn from(mut o: #struct_ident) -> crate::protobuf::#pb_ty { fn from(mut o: #struct_ident) -> crate::protobuf::#pb_ty {
let mut pb = crate::protobuf::#pb_ty::new(); let mut pb = crate::protobuf::#pb_ty::new();