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(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
const LoadingState._();
isLoading() => this is _Loading;
}

View File

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

View File

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

View File

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

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';
class AuthError {
static final supabaseSignInError = FlowyError()
..msg = 'supabase sign in error'
..code = -10001;
..msg = 'supabase sign in error -10001'
..code = ErrorCode.UserUnauthorized;
static final supabaseSignUpError = FlowyError()
..msg = 'supabase sign up error'
..code = -10002;
..msg = 'supabase sign up error -10002'
..code = ErrorCode.UserUnauthorized;
static final supabaseSignInWithOauthError = FlowyError()
..msg = 'supabase sign in with oauth error'
..code = -10003;
..msg = 'supabase sign in with oauth error -10003'
..code = ErrorCode.UserUnauthorized;
static final supabaseGetUserError = FlowyError()
..msg = 'unable to get user from supabase'
..code = -10004;
..msg = 'unable to get user from supabase -10004'
..code = ErrorCode.UserUnauthorized;
}

View File

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

View File

@ -119,7 +119,7 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
}
SignUpState stateFromCode(FlowyError error) {
switch (ErrorCode.valueOf(error.code)!) {
switch (error.code) {
case ErrorCode.EmailFormatInvalid:
return state.copyWith(
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/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -49,9 +48,8 @@ class _EncryptSecretScreenState extends State<EncryptSecretScreen> {
(unit) async {
await runAppFlowy();
},
(err) {
Log.error(err);
showSnackBarMessage(context, err.msg);
(error) {
handleOpenWorkspaceError(context, error);
},
);
},

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

View File

@ -3,10 +3,14 @@ import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/background.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
@ -14,7 +18,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
@ -62,7 +65,26 @@ class SignInScreen extends StatelessWidget {
router.pushHomeScreen(context, user);
}
},
(error) => showSnapBar(context, error.msg),
(error) {
handleOpenWorkspaceError(context, error);
},
);
}
}
void handleOpenWorkspaceError(BuildContext context, FlowyError error) {
if (error.code == ErrorCode.WorkspaceDataNotSync) {
final userFolder = UserFolderPB.fromBuffer(error.payload);
getIt<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/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/sign_in_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter/material.dart';
@ -80,9 +81,8 @@ class SplashScreen extends StatelessWidget {
workspaceSetting,
);
},
(error) async {
Log.error(error);
getIt<SplashRoute>().pushWelcomeScreen(context, userProfile);
(error) {
handleOpenWorkspaceError(context, error);
},
);
}

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(
constraints: const BoxConstraints(
maxWidth: 300,
maxHeight: 100,
maxWidth: 400,
maxHeight: 260,
),
child: FlowyText.medium(
widget.title,

View File

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

View File

@ -45,6 +45,8 @@
},
"workspace": {
"create": "Create workspace",
"reset": "Reset workspace",
"resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace",
"hint": "workspace",
"notFoundError": "Workspace not found"
},

View File

@ -29,7 +29,7 @@ pub(crate) async fn get_key_value_handler(
data: AFPluginData<KeyPB>,
) -> DataResult<KeyValuePB, FlowyError> {
match store_preferences.upgrade() {
None => Err(FlowyError::internal().context("The store preferences is already drop"))?,
None => Err(FlowyError::internal().with_context("The store preferences is already drop"))?,
Some(store_preferences) => {
let data = data.into_inner();
let value = store_preferences.get_str(&data.key);
@ -46,7 +46,7 @@ pub(crate) async fn remove_key_value_handler(
data: AFPluginData<KeyPB>,
) -> FlowyResult<()> {
match store_preferences.upgrade() {
None => Err(FlowyError::internal().context("The store preferences is already drop"))?,
None => Err(FlowyError::internal().with_context("The store preferences is already drop"))?,
Some(store_preferences) => {
let data = data.into_inner();
store_preferences.remove(&data.key);

View File

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

View File

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

View File

@ -69,7 +69,7 @@ impl FolderUser for FolderUserImpl {
self
.0
.upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))?
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.user_id()
}
@ -77,7 +77,7 @@ impl FolderUser for FolderUserImpl {
self
.0
.upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))?
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.token()
}
@ -85,7 +85,7 @@ impl FolderUser for FolderUserImpl {
self
.0
.upgrade()
.ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))?
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.get_collab_db(uid)
}
}
@ -305,7 +305,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
ViewLayout::Calendar => make_default_calendar(view_id, &name),
ViewLayout::Document => {
return FutureResult::new(async move {
Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
Err(FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)))
});
},
};
@ -332,7 +332,8 @@ impl FolderOperationHandler for DatabaseFolderOperation {
_ => CSVFormat::Original,
};
FutureResult::new(async move {
let content = String::from_utf8(bytes).map_err(|err| FlowyError::internal().context(err))?;
let content =
String::from_utf8(bytes).map_err(|err| FlowyError::internal().with_context(err))?;
database_manager
.import_csv(view_id, content, format)
.await?;
@ -359,7 +360,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
let database_layout = match new.layout {
ViewLayout::Document => {
return FutureResult::new(async {
Err(FlowyError::internal().context("Can't handle document layout type"))
Err(FlowyError::internal().with_context("Can't handle document layout type"))
});
},
ViewLayout::Grid => DatabaseLayoutPB::Grid,

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> {
let collab_db = open_collab_db(&self.storage_path, uid)?;
let read_txn = collab_db.read_txn();
let updates = read_txn
.get_all_updates(uid, object_id)
.map_err(|e| FlowyError::internal().context(format!("Failed to open collab db: {:?}", e)))?;
let updates = read_txn.get_all_updates(uid, object_id).map_err(|e| {
FlowyError::internal().with_context(format!("Failed to open collab db: {:?}", e))
})?;
Ok(updates)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -418,7 +418,7 @@ impl DatabaseViewEditor {
.as_ref()
.and_then(|group| group.get_group(group_id))
{
None => Err(FlowyError::record_not_found().context("Can't find the group")),
None => Err(FlowyError::record_not_found().with_context("Can't find the group")),
Some((_, group)) => Ok(GroupPB::from(group)),
}
}

View File

@ -125,7 +125,10 @@ impl CellDataChangeset for RichTextTypeOption {
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
if changeset.len() > 10000 {
Err(FlowyError::text_too_long().context("The len of the text should not be more than 10000"))
Err(
FlowyError::text_too_long()
.with_context("The len of the text should not be more than 10000"),
)
} else {
let text_cell_data = StrCellData(changeset);
Ok((text_cell_data.clone().into(), text_cell_data))

View File

@ -223,7 +223,9 @@ where
})?;
Ok(())
},
_ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")),
_ => Err(
FlowyError::record_not_found().with_context("Moving group failed. Groups are not exist"),
),
}
}

View File

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

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

View File

@ -22,7 +22,7 @@ fn upgrade_document(
) -> FlowyResult<Arc<DocumentManager>> {
let manager = document_manager
.upgrade()
.ok_or(FlowyError::internal().context("The document manager is already dropped"))?;
.ok_or(FlowyError::internal().with_context("The document manager is already dropped"))?;
Ok(manager)
}

View File

@ -3,9 +3,12 @@ use thiserror::Error;
use flowy_derive::ProtoBuf_Enum;
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize_repr, Deserialize_repr, ProtoBuf_Enum)]
#[derive(
Debug, Default, Clone, PartialEq, Eq, Error, Serialize_repr, Deserialize_repr, ProtoBuf_Enum,
)]
#[repr(u8)]
pub enum ErrorCode {
#[default]
#[error("Internal error")]
Internal = 0,
@ -226,6 +229,9 @@ pub enum ErrorCode {
#[error("It appears that the collaboration object's data has not been fully synchronized")]
CollabDataNotSync = 75,
#[error("It appears that the workspace data has not been fully synchronized")]
WorkspaceDataNotSync = 76,
}
impl ErrorCode {

View File

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

View File

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

View File

@ -2,6 +2,6 @@ use crate::FlowyError;
impl std::convert::From<tokio_postgres::Error> for FlowyError {
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 crate::FlowyError;
impl std::convert::From<reqwest::Error> for FlowyError {
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 {
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::ViewLayoutPB;
use crate::share::{ImportParams, ImportType};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::FlowyError;
#[derive(Clone, Debug, ProtoBuf_Enum)]
pub enum ImportTypePB {
HistoryDocument = 0,
@ -69,7 +69,7 @@ impl TryInto<ImportParams> for ImportPB {
None => None,
Some(file_path) => Some(
NotEmptyStr::parse(file_path)
.map_err(|_| FlowyError::invalid_data().context("The import file path is empty"))?
.map_err(|_| FlowyError::invalid_data().with_context("The import file path is empty"))?
.0,
),
};

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::{
entities::parser::workspace::{WorkspaceDesc, WorkspaceIdentify, WorkspaceName},
entities::view::ViewPB,
};
use collab::core::collab_state::SyncState;
use collab_folder::core::Workspace;
use flowy_derive::ProtoBuf;
use flowy_error::ErrorCode;
use std::convert::TryInto;
#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)]
pub struct WorkspacePB {
@ -197,3 +200,12 @@ impl From<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>> {
let folder = folder_manager
.upgrade()
.ok_or(FlowyError::internal().context("The folder manager is already dropped"))?;
.ok_or(FlowyError::internal().with_context("The folder manager is already dropped"))?;
Ok(folder)
}
@ -45,10 +45,10 @@ pub(crate) async fn open_workspace_handler(
let folder = upgrade_folder(folder)?;
let params: WorkspaceIdPB = data.into_inner();
match params.value {
None => Err(FlowyError::workspace_id().context("workspace id should not be empty")),
None => Err(FlowyError::workspace_id().with_context("workspace id should not be empty")),
Some(workspace_id) => {
if workspace_id.is_empty() {
Err(FlowyError::workspace_id().context("workspace id should not be empty"))
Err(FlowyError::workspace_id().with_context("workspace id should not be empty"))
} else {
let workspace = folder.open_workspace(&workspace_id).await?;
let views = folder.get_workspace_views(&workspace_id).await?;

View File

@ -37,8 +37,8 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin {
.event(FolderEvent::RestoreAllTrash, restore_all_trash_handler)
.event(FolderEvent::DeleteAllTrash, delete_all_trash_handler)
.event(FolderEvent::ImportData, import_data_handler)
.event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler)
.event(FolderEvent::UpdateViewIcon, update_view_icon_handler)
.event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler)
.event(FolderEvent::UpdateViewIcon, update_view_icon_handler)
.event(FolderEvent::ReadFavorites, read_favorites_handler)
.event(FolderEvent::ToggleFavorite, toggle_favorites_handler)
}
@ -134,7 +134,7 @@ pub enum FolderEvent {
#[event(input = "ImportPB")]
ImportData = 30,
#[event()]
#[event(input = "WorkspaceIdPB", output = "RepeatedFolderSnapshotPB")]
GetFolderSnapshots = 31,
/// Moves a nested view to a new location in the hierarchy.
///

View File

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

View File

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

View File

@ -159,7 +159,7 @@ impl HttpRequestBuilder {
fn unexpected_empty_payload(url: &str) -> FlowyError {
let msg = format!("Request: {} receives unexpected empty payload", url);
FlowyError::payload_none().context(msg)
FlowyError::payload_none().with_context(msg)
}
async fn flowy_response_from(original: Response) -> Result<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)),
}
} 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![]) })
}
fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> {
// TODO(nathan): implement the RESTful API for this
FutureResult::new(async { Ok(()) })
}
fn create_collab_object(
&self,
_collab_object: &CollabObject,

View File

@ -161,8 +161,7 @@ where
.get_workspace_id()
.ok_or(anyhow::anyhow!("Invalid workspace id"))?;
let update_items =
get_updates_from_server(&object.object_id, &object.ty, postgrest.clone()).await?;
let update_items = get_updates_from_server(&object.object_id, &object.ty, &postgrest).await?;
// If the update_items is empty, we can send the init_update directly
if update_items.is_empty() {
@ -175,32 +174,7 @@ where
)
.await?;
} else {
// 2.Merge the updates into one and then delete the merged updates
let merge_result = spawn_blocking(move || merge_updates(update_items, init_update)).await??;
tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len());
let value_size = merge_result.new_update.len() as i32;
let md5 = md5(&merge_result.new_update);
let (new_update, encrypt) =
SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &self.secret())?;
let params = InsertParamsBuilder::new()
.insert("oid", object.object_id.clone())
.insert("new_value", new_update)
.insert("encrypt", encrypt)
.insert("md5", md5)
.insert("value_size", value_size)
.insert("partition_key", partition_key(&object.ty))
.insert("uid", object.uid)
.insert("workspace_id", workspace_id)
.insert("removed_keys", merge_result.merged_keys)
.insert("did", object.get_device_id())
.build();
postgrest
.rpc("flush_collab_updates_v3", params)
.execute()
.await?
.success()
flush_collab_with_update(object, update_items, &postgrest, init_update, self.secret())
.await?;
}
Ok(())
@ -215,6 +189,49 @@ where
}
}
pub(crate) async fn flush_collab_with_update(
object: &CollabObject,
update_items: Vec<UpdateItem>,
postgrest: &Arc<PostgresWrapper>,
update: Vec<u8>,
secret: Option<String>,
) -> Result<(), Error> {
// 2.Merge the updates into one and then delete the merged updates
let merge_result = spawn_blocking(move || merge_updates(update_items, update)).await??;
tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len());
let workspace_id = object
.get_workspace_id()
.ok_or(anyhow::anyhow!("Invalid workspace id"))?;
let value_size = merge_result.new_update.len() as i32;
let md5 = md5(&merge_result.new_update);
tracing::trace!("Flush collab id:{} type:{}", object.object_id, object.ty);
let (new_update, encrypt) =
SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &secret)?;
let params = InsertParamsBuilder::new()
.insert("oid", object.object_id.clone())
.insert("new_value", new_update)
.insert("encrypt", encrypt)
.insert("md5", md5)
.insert("value_size", value_size)
.insert("partition_key", partition_key(&object.ty))
.insert("uid", object.uid)
.insert("workspace_id", workspace_id)
.insert("removed_keys", merge_result.merged_keys)
.insert("did", object.get_device_id())
.build();
postgrest
.rpc("flush_collab_updates_v3", params)
.execute()
.await?
.success()
.await?;
Ok(())
}
pub(crate) async fn send_update(
workspace_id: String,
object: &CollabObject,

View File

@ -72,7 +72,7 @@ where
let workspace_id = workspace_id.to_string();
FutureResult::new(async move {
let postgrest = try_get_postgrest?;
let updates = get_updates_from_server(&workspace_id, &CollabType::Folder, postgrest).await?;
let updates = get_updates_from_server(&workspace_id, &CollabType::Folder, &postgrest).await?;
let updates = updates
.into_iter()
.map(|item| item.value)

View File

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

View File

@ -2,23 +2,29 @@ use std::str::FromStr;
use std::sync::{Arc, Weak};
use anyhow::Error;
use collab::core::collab::MutexCollab;
use collab::core::origin::CollabOrigin;
use collab_plugins::cloud_storage::CollabObject;
use parking_lot::RwLock;
use serde_json::Value;
use tokio::sync::oneshot::channel;
use uuid::Uuid;
use flowy_folder_deps::cloud::{Folder, Workspace};
use flowy_user_deps::cloud::*;
use flowy_user_deps::entities::*;
use flowy_user_deps::DEFAULT_USER_NAME;
use lib_infra::box_any::BoxAny;
use lib_infra::future::FutureResult;
use lib_infra::util::timestamp;
use crate::supabase::api::request::FetchObjectUpdateAction;
use crate::supabase::api::request::{get_updates_from_server, FetchObjectUpdateAction};
use crate::supabase::api::util::{
ExtendedResponse, InsertParamsBuilder, RealtimeBinaryColumnDecoder, SupabaseBinaryColumnDecoder,
};
use crate::supabase::api::{send_update, PostgresWrapper, SupabaseServerService};
use crate::supabase::api::{
flush_collab_with_update, send_update, PostgresWrapper, SupabaseServerService,
};
use crate::supabase::define::*;
use crate::supabase::entities::UserProfileResponse;
use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent};
@ -266,6 +272,39 @@ where
self.user_update_tx.as_ref().map(|tx| tx.subscribe())
}
fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error> {
let collab_object = collab_object.clone();
let try_get_postgrest = self.server.try_get_weak_postgrest();
let (tx, rx) = channel();
let init_update = empty_workspace_update(&collab_object);
tokio::spawn(async move {
tx.send(
async move {
let postgrest = try_get_postgrest?
.upgrade()
.ok_or(anyhow::anyhow!("postgrest is not available"))?;
let updates =
get_updates_from_server(&collab_object.object_id, &collab_object.ty, &postgrest)
.await?;
flush_collab_with_update(
&collab_object,
updates,
&postgrest,
init_update,
postgrest.secret(),
)
.await?;
Ok(())
}
.await,
)
});
FutureResult::new(async { rx.await? })
}
fn create_collab_object(
&self,
collab_object: &CollabObject,
@ -516,3 +555,21 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler {
}
}
}
fn empty_workspace_update(collab_object: &CollabObject) -> Vec<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
.error()
.unwrap();
assert_eq!(error.code, ErrorCode::RecordNotFound.value());
assert_eq!(error.code, ErrorCode::RecordNotFound);
}
#[tokio::test]
@ -150,7 +150,7 @@ async fn put_back_trash_event_test() {
.await
.error()
.unwrap();
assert_eq!(error.code, ErrorCode::RecordNotFound.value());
assert_eq!(error.code, ErrorCode::RecordNotFound);
let payload = TrashIdPB {
id: view.id.clone(),
@ -480,7 +480,7 @@ async fn create_parent_view_with_invalid_name() {
.error()
.unwrap()
.code,
code.value()
code
)
}
}

View File

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

View File

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

View File

@ -103,7 +103,7 @@ async fn third_party_sign_up_with_duplicated_email() {
.await
.err()
.unwrap();
assert_eq!(error.code, ErrorCode::Conflict.value());
assert_eq!(error.code, ErrorCode::Conflict);
};
}
@ -198,7 +198,7 @@ async fn check_not_exist_user_test() {
.check_user_with_uuid(&uuid::Uuid::new_v4().to_string())
.await
.unwrap_err();
assert_eq!(err.code, ErrorCode::RecordNotFound.value());
assert_eq!(err.code, ErrorCode::RecordNotFound);
}
}
@ -256,6 +256,6 @@ async fn update_user_profile_with_existing_email_test() {
)
.await
.unwrap();
assert_eq!(error.code, ErrorCode::Conflict.value());
assert_eq!(error.code, ErrorCode::Conflict);
}
}

View File

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

View File

@ -48,6 +48,9 @@ pub struct UserProfilePB {
#[pb(index = 9)]
pub encryption_type: EncryptionTypePB,
#[pb(index = 10)]
pub workspace_id: String,
}
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
@ -78,6 +81,7 @@ impl std::convert::From<UserProfile> for UserProfilePB {
auth_type: user_profile.auth_type.into(),
encryption_sign,
encryption_type: encryption_ty,
workspace_id: user_profile.workspace_id,
}
}
}
@ -274,3 +278,12 @@ impl From<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 flowy_error::{FlowyError, FlowyResult};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_sqlite::kv::StorePreferences;
use flowy_user_deps::cloud::UserCloudConfig;
use flowy_user_deps::entities::*;
@ -20,7 +20,7 @@ use crate::services::cloud_config::{
fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc<UserManager>> {
let manager = manager
.upgrade()
.ok_or(FlowyError::internal().context("The user session is already drop"))?;
.ok_or(FlowyError::internal().with_context("The user session is already drop"))?;
Ok(manager)
}
@ -29,7 +29,7 @@ fn upgrade_store_preferences(
) -> FlowyResult<Arc<StorePreferences>> {
let store = store
.upgrade()
.ok_or(FlowyError::internal().context("The store preferences is already drop"))?;
.ok_or(FlowyError::internal().with_context("The store preferences is already drop"))?;
Ok(store)
}
@ -96,7 +96,15 @@ pub async fn get_user_profile_handler(
let manager = upgrade_manager(manager)?;
let uid = manager.get_session()?.user_id;
let user_profile = manager.get_user_profile(uid).await?;
let _ = manager.refresh_user_profile(&user_profile).await;
let weak_manager = Arc::downgrade(&manager);
let cloned_user_profile = user_profile.clone();
tokio::spawn(async move {
if let Some(manager) = weak_manager.upgrade() {
let _ = manager.refresh_user_profile(&cloned_user_profile).await;
}
});
data_result_ok(user_profile.into())
}
@ -250,7 +258,7 @@ pub async fn set_cloud_config_handler(
let update = data.into_inner();
let store_preferences = upgrade_store_preferences(store_preferences)?;
let mut config = get_cloud_config(session.user_id, &store_preferences)
.ok_or(FlowyError::internal().context("Can't find any cloud config"))?;
.ok_or(FlowyError::internal().with_context("Can't find any cloud config"))?;
if let Some(enable_sync) = update.enable_sync {
manager.cloud_services.set_enable_sync(enable_sync);
@ -429,3 +437,20 @@ pub async fn get_all_reminder_event_handler(
.collect::<Vec<_>>();
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::CreateReminder, create_reminder_event_handler)
.event(UserEvent::GetAllReminders, get_all_reminder_event_handler)
.event(UserEvent::ResetWorkspace, reset_workspace_handler)
}
pub struct SignUpContext {
@ -271,4 +272,7 @@ pub enum UserEvent {
#[event(output = "RepeatedReminderPB")]
GetAllReminders = 29,
#[event(input = "ResetWorkspacePB")]
ResetWorkspace = 30,
}

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);
tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir);
let db = flowy_sqlite::init(&dir)
.map_err(|e| FlowyError::internal().context(format!("open user db failed, {:?}", e)))?;
.map_err(|e| FlowyError::internal().with_context(format!("open user db failed, {:?}", e)))?;
let pool = db.get_pool();
write_guard.insert(user_id.to_owned(), db);
drop(write_guard);

View File

@ -1,12 +1,14 @@
use std::convert::TryFrom;
use std::sync::Arc;
use appflowy_integrate::{CollabObject, CollabType};
use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::schema::user_workspace_table;
use flowy_sqlite::{query_dsl::*, ConnectionPool, ExpressionMethods};
use flowy_user_deps::entities::UserWorkspace;
use crate::entities::RepeatedUserWorkspacePB;
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB};
use crate::manager::UserManager;
use crate::notification::{send_notification, UserNotification};
use crate::services::user_workspace_sql::UserWorkspaceTable;
@ -84,6 +86,20 @@ impl UserManager {
}
Ok(rows.into_iter().map(UserWorkspace::from).collect())
}
/// Reset the remote workspace using local workspace data. This is useful when a user wishes to
/// open a workspace on a new device that hasn't fully synchronized with the server.
pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> {
let collab_object =
CollabObject::new(reset.uid, reset.workspace_id.clone(), CollabType::Folder)
.with_workspace_id(reset.workspace_id);
self
.cloud_services
.get_user_service()?
.reset_workspace(collab_object)
.await?;
Ok(())
}
}
pub fn save_user_workspaces(

View File

@ -21,10 +21,10 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
fn try_from(value: (i64, &UserWorkspace)) -> Result<Self, Self::Error> {
if value.1.id.is_empty() {
return Err(FlowyError::invalid_data().context("The id is empty"));
return Err(FlowyError::invalid_data().with_context("The id is empty"));
}
if value.1.database_storage_id.is_empty() {
return Err(FlowyError::invalid_data().context("The database storage id is empty"));
return Err(FlowyError::invalid_data().with_context("The database storage id is empty"));
}
Ok(Self {

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 {
fn from(mut o: #struct_ident) -> crate::protobuf::#pb_ty {
let mut pb = crate::protobuf::#pb_ty::new();