From a49b009980b850c7b29f9253a5ad68356b554f80 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 26 Dec 2023 02:03:42 +0800 Subject: [PATCH] tests: more cloud test (#4204) * test: add anon user test * chore: add to runner * test: fix * test: fix * test: add tests * chore: add test * chore: fix warn * chore: fix warn * fix: build * fix: test * chore: rename * chore: fix test * chore: fix test * chore: fix test * chore: disable test --- .github/workflows/rust_ci.yaml | 25 ++++ .../assets/test/workspaces/039_local.zip | Bin 47437 -> 0 bytes .../appearance_settings_test.dart | 4 +- .../cloud/anon_user_continue_test.dart | 68 +++++++++++ .../cloud/anon_user_to_cloud_test.dart | 74 ++++++++++++ .../cloud/appflowy_cloud_auth_test.dart | 2 +- .../integration_test/cloud/cloud_runner.dart | 7 +- .../cloud/document_sync_test.dart | 7 +- .../cloud/supabase_auth_test.dart | 2 +- .../cloud/user_setting_sync_test.dart | 4 +- .../integration_test/hotkeys_test.dart | 4 +- .../settings/user_language_test.dart | 2 +- .../integration_test/switch_folder_test.dart | 10 +- .../integration_test/util/auth_operation.dart | 11 ++ .../integration_test/util/base.dart | 73 +++++++----- .../integration_test/util/dir.dart | 23 ++++ .../integration_test/util/expectation.dart | 12 +- .../integration_test/util/settings.dart | 3 +- .../lib/startup/deps_resolver.dart | 2 +- .../lib/startup/entry_point.dart | 2 +- .../appflowy_flutter/lib/startup/startup.dart | 39 +++++- .../startup/tasks/appflowy_cloud_task.dart | 2 +- .../auth/af_cloud_auth_service.dart | 2 +- .../auth/af_cloud_mock_auth_service.dart | 6 +- .../auth/backend_auth_service.dart | 6 +- .../auth/supabase_auth_service.dart | 4 +- .../auth/supabase_mock_auth_service.dart | 4 +- .../lib/user/presentation/anon_user.dart | 5 +- .../screens/skip_log_in_screen.dart | 7 +- .../home/menu/sidebar/sidebar_user.dart | 29 +++-- ...settings_file_customize_location_view.dart | 13 +- .../settings/widgets/settings_user_view.dart | 7 +- frontend/appflowy_flutter/test/util.dart | 4 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 38 +++--- frontend/appflowy_tauri/src-tauri/Cargo.toml | 18 +-- frontend/rust-lib/Cargo.lock | 38 +++--- frontend/rust-lib/Cargo.toml | 18 +-- frontend/rust-lib/dart-ffi/Cargo.toml | 2 +- .../rust-lib/event-integration/src/lib.rs | 6 +- .../event-integration/src/user_event.rs | 14 +-- .../user/af_cloud_test/anon_user_test.rs | 111 +++++++----------- .../tests/user/local_test/auth_test.rs | 10 +- .../user/local_test/user_profile_test.rs | 4 +- .../tests/user/supabase_test/auth_test.rs | 10 +- .../user/supabase_test/workspace_test.rs | 4 +- .../rust-lib/event-integration/tests/util.rs | 6 +- .../flowy-core/src/integrate/server.rs | 70 ++++++----- .../flowy-core/src/integrate/trait_impls.rs | 20 ++-- frontend/rust-lib/flowy-core/src/lib.rs | 12 +- .../flowy-document2/src/parser/utils.rs | 1 - .../rust-lib/flowy-folder2/src/manager.rs | 7 +- .../flowy-folder2/src/manager_init.rs | 6 +- .../flowy-folder2/src/manager_observer.rs | 41 ++++--- .../flowy-folder2/src/user_default.rs | 1 + .../src/af_cloud/impls/database.rs | 24 ++-- .../src/af_cloud/impls/document.rs | 14 ++- .../flowy-server/src/af_cloud/impls/folder.rs | 16 ++- .../af_cloud/impls/user/cloud_service_impl.rs | 14 ++- .../src/local_server/impls/user.rs | 1 + .../flowy-server/src/supabase/api/user.rs | 17 ++- .../rust-lib/flowy-user-deps/src/cloud.rs | 1 + .../migrate_anon_user_collab.rs | 43 ++++++- .../anon_user_upgrade/sync_af_user_collab.rs | 12 +- .../sync_supabase_user_collab.rs | 12 +- .../rust-lib/flowy-user/src/entities/auth.rs | 16 +-- .../flowy-user/src/entities/user_profile.rs | 6 +- .../flowy-user/src/entities/user_setting.rs | 11 +- frontend/rust-lib/flowy-user/src/manager.rs | 18 ++- .../rust-lib/flowy-user/src/migrations/mod.rs | 4 +- .../src/migrations/session_migration.rs | 24 ++-- .../src/migrations/workspace_trash_v1.rs | 56 +++++++++ .../flowy-user/src/services/entities.rs | 22 ++-- frontend/rust-lib/lib-log/src/lib.rs | 11 +- frontend/scripts/makefile/tests.toml | 4 +- 74 files changed, 776 insertions(+), 450 deletions(-) delete mode 100644 frontend/appflowy_flutter/assets/test/workspaces/039_local.zip create mode 100644 frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart create mode 100644 frontend/appflowy_flutter/integration_test/cloud/anon_user_to_cloud_test.dart create mode 100644 frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 785966e00a..ced3afcb0f 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -68,6 +68,31 @@ jobs: echo SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} >> .env.ci echo SUPABASE_JWT_SECRET=${{ secrets.SUPABASE_JWT_SECRET }} >> .env.ci + - name: Checkout appflowy cloud code + uses: actions/checkout@v3 + with: + repository: AppFlowy-IO/AppFlowy-Cloud + path: AppFlowy-Cloud + depth: 1 + + - name: Prepare appflowy cloud env + working-directory: AppFlowy-Cloud + run: | + # log level + cp dev.env .env + sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env + sed -i 's/GOTRUE_SMTP_USER=.*/GOTRUE_SMTP_USER=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_USER }}/' .env + sed -i 's/GOTRUE_SMTP_PASS=.*/GOTRUE_SMTP_PASS=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_PASS }}/' .env + sed -i 's/GOTRUE_SMTP_ADMIN_EMAIL=.*/GOTRUE_SMTP_ADMIN_EMAIL=${{ secrets.INTEGRATION_TEST_GOTRUE_SMTP_ADMIN_EMAIL }}/' .env + sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env + sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env + cat .env + + - name: Run Docker-Compose + working-directory: AppFlowy-Cloud + run: | + docker compose up -d + - name: Run rust-lib tests working-directory: frontend/rust-lib run: RUST_LOG=info RUST_BACKTRACE=1 cargo test --no-default-features --features="rev-sqlite" diff --git a/frontend/appflowy_flutter/assets/test/workspaces/039_local.zip b/frontend/appflowy_flutter/assets/test/workspaces/039_local.zip deleted file mode 100644 index 7ef72daa02b7ff29c07f80ef3af531e3efbab1a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47437 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 diff --git a/frontend/appflowy_flutter/integration_test/appearance_settings_test.dart b/frontend/appflowy_flutter/integration_test/appearance_settings_test.dart index 8bace6f2eb..696b43c015 100644 --- a/frontend/appflowy_flutter/integration_test/appearance_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/appearance_settings_test.dart @@ -16,7 +16,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.openSettings(); await tester.openSettingsPage(SettingsPage.appearance); @@ -48,7 +48,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.openSettings(); await tester.openSettingsPage(SettingsPage.appearance); diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart new file mode 100644 index 0000000000..68a3fd3ed5 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart @@ -0,0 +1,68 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:integration_test/integration_test.dart'; +import '../util/dir.dart'; +import '../util/mock/mock_file_picker.dart'; +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('appflowy cloud', () { + testWidgets('anon user and then sign in', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloud, + ); + + tester.expectToSeeText(LocaleKeys.signIn_loginStartWithAnonymous.tr()); + await tester.tapGoButton(); + await tester.expectToSeeHomePage(); + + // reanme the name of the anon user + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + final userNameFinder = find.descendant( + of: find.byType(SettingsUserView), + matching: find.byType(UserNameInput), + ); + await tester.enterText(userNameFinder, 'local_user'); + await tester.pumpAndSettle(); + + // sign up with Google + await tester.tapGoogleLoginInButton(); + await tester.pumpAndSettle(); + + // sign out + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + await tester.logout(); + await tester.pumpAndSettle(); + + // tap the continue as anonymous button + await tester + .tapButton(find.text(LocaleKeys.signIn_continueAnonymousUser.tr())); + await tester.expectToSeeHomePage(); + + // assert the name of the anon user is local_user + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + final userNameInput = tester.widget(userNameFinder) as UserNameInput; + expect(userNameInput.name, 'local_user'); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_to_cloud_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_to_cloud_test.dart new file mode 100644 index 0000000000..68d89252f0 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_to_cloud_test.dart @@ -0,0 +1,74 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as p; +import 'package:integration_test/integration_test.dart'; +import '../util/database_test_op.dart'; +import '../util/dir.dart'; +import '../util/mock/mock_file_picker.dart'; +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('appflowy cloud', () { + testWidgets('anon user -> af cloud -> continue anon', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloud, + ); + + tester.expectToSeeText(LocaleKeys.signIn_loginStartWithAnonymous.tr()); + await tester.tapGoButton(); + await tester.expectToSeeHomePage(); + + // reanme the name of the anon user + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + final userNameFinder = find.descendant( + of: find.byType(SettingsUserView), + matching: find.byType(UserNameInput), + ); + await tester.enterText(userNameFinder, 'local_user'); + await tester.openSettingsPage(SettingsPage.user); + await tester.tapEscButton(); + await tester.expectToSeeHomePage(); + + // sign up with Google + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + await tester.tapGoogleLoginInButton(); + + await tester.expectToSeeHomePage(); + + // sign out + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + await tester.logout(); + await tester.pumpAndSettle(); + + tester.expectToSeeText(LocaleKeys.signIn_continueAnonymousUser.tr()); + // tap the continue as anonymous button + await tester.tapButton(find.byType(SignInAnonymousButton)); + + await tester.expectToSeeHomePage(); + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.user); + final userNameInput = tester.widget(userNameFinder) as UserNameInput; + expect(userNameInput.name, 'local_user'); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart index 72eb62631d..8e8ecff0e2 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart @@ -25,7 +25,7 @@ void main() { cloudType: AuthenticatorType.appflowyCloud, ); await tester.tapGoogleLoginInButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); }); testWidgets('sign out', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart index 58c1abdb0a..805fbc4dc8 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart @@ -1,14 +1,17 @@ -import 'empty_test.dart' as empty_test; +import 'empty_test.dart' as preset_af_cloud_env_test; import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test; import 'document_sync_test.dart' as document_sync_test; import 'user_setting_sync_test.dart' as user_sync_test; +// import 'anon_user_continue_test.dart' as anon_user_continue_test; Future main() async { - empty_test.main(); + preset_af_cloud_env_test.main(); appflowy_cloud_auth_test.main(); document_sync_test.main(); user_sync_test.main(); + + // anon_user_continue_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart index 07fd8caccd..798b1c7473 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart @@ -35,7 +35,7 @@ void main() { email: email, ); await tester.tapGoogleLoginInButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); // create a new document called Sample await tester.createNewPageWithName( @@ -52,8 +52,7 @@ void main() { await tester.openSettings(); await tester.openSettingsPage(SettingsPage.user); - await tester.tapButton(find.byType(SettingLogoutButton)); - tester.expectToSeeText(LocaleKeys.button_ok.tr()); + await tester.logout(); }); testWidgets('sync doc from server', (tester) async { @@ -62,7 +61,7 @@ void main() { email: email, ); await tester.tapGoogleLoginInButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.pumpAndSettle(const Duration(seconds: 2)); // The document will be synced from the server diff --git a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart index 34aff21fee..8079a2da69 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart @@ -15,7 +15,7 @@ void main() { testWidgets('sign in with supabase', (tester) async { await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase); await tester.tapGoogleLoginInButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); }); testWidgets('sign out with supabase', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart index 78aecf1c08..ec52540c39 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart @@ -36,7 +36,7 @@ void main() { email: email, ); await tester.tapGoogleLoginInButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.openSettings(); await tester.openSettingsPage(SettingsPage.user); @@ -74,7 +74,7 @@ void main() { email: email, ); await tester.tapGoogleLoginInButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.pumpAndSettle(); await tester.openSettings(); diff --git a/frontend/appflowy_flutter/integration_test/hotkeys_test.dart b/frontend/appflowy_flutter/integration_test/hotkeys_test.dart index 6cc2f484a7..3f216010e0 100644 --- a/frontend/appflowy_flutter/integration_test/hotkeys_test.dart +++ b/frontend/appflowy_flutter/integration_test/hotkeys_test.dart @@ -19,7 +19,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.openSettings(); await tester.openSettingsPage(SettingsPage.appearance); @@ -71,7 +71,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/integration_test/settings/user_language_test.dart b/frontend/appflowy_flutter/integration_test/settings/user_language_test.dart index 6bc4c07bb8..d9aa28812c 100644 --- a/frontend/appflowy_flutter/integration_test/settings/user_language_test.dart +++ b/frontend/appflowy_flutter/integration_test/settings/user_language_test.dart @@ -14,7 +14,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapGoButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); await tester.openSettings(); await tester.openSettingsPage(SettingsPage.language); diff --git a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart index ded49d3bd2..341fef0446 100644 --- a/frontend/appflowy_flutter/integration_test/switch_folder_test.dart +++ b/frontend/appflowy_flutter/integration_test/switch_folder_test.dart @@ -33,7 +33,7 @@ void main() { ); await tester.tapGoButton(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); // switch to user B { @@ -51,7 +51,7 @@ void main() { ); await tester.tapCustomLocationButton(); await tester.pumpAndSettle(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); // set user name for userB await tester.openSettings(); @@ -71,7 +71,7 @@ void main() { await tester.tapCustomLocationButton(); await tester.pumpAndSettle(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); tester.expectToSeeUserName(userA); } @@ -88,7 +88,7 @@ void main() { await tester.tapCustomLocationButton(); await tester.pumpAndSettle(); - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); tester.expectToSeeUserName(userB); } }); @@ -99,7 +99,7 @@ void main() { await tester.tapGoButton(); // home and readme document - tester.expectToSeeHomePage(); + await tester.expectToSeeHomePage(); // open settings and restore the location await tester.openSettings(); diff --git a/frontend/appflowy_flutter/integration_test/util/auth_operation.dart b/frontend/appflowy_flutter/integration_test/util/auth_operation.dart index 688a246113..1119c428e1 100644 --- a/frontend/appflowy_flutter/integration_test/util/auth_operation.dart +++ b/frontend/appflowy_flutter/integration_test/util/auth_operation.dart @@ -1,16 +1,27 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; +import 'expectation.dart'; extension AppFlowyAuthTest on WidgetTester { Future tapGoogleLoginInButton() async { await tapButton(find.byKey(const Key('signInWithGoogleButton'))); } + Future logout() async { + await tapButton(find.byType(SettingLogoutButton)); + + expectToSeeText(LocaleKeys.button_ok.tr()); + await tapButtonWithName(LocaleKeys.button_ok.tr()); + } + Future tapSignInAsGuest() async { await tapButton(find.byType(SignInAnonymousButton)); } diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index b34ac866f5..5df7e9edb8 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -15,7 +15,6 @@ import 'package:dartz/dartz.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; @@ -33,21 +32,24 @@ extension AppFlowyTestBase on WidgetTester { Future initializeAppFlowy({ // use to append after the application data directory String? pathExtension, + // use to specify the application data directory, if not specified, a temporary directory will be used. + String? dataDirectory, Size windowsSize = const Size(1600, 1200), AuthenticatorType? cloudType, String? email, }) async { + // view.physicalSize = windowsSize; binding.setSurfaceSize(windowsSize); + // addTearDown(() => binding.setSurfaceSize(null)); mockHotKeyManagerHandlers(); - final directory = await mockApplicationDataStorage( - pathExtension: pathExtension, - ); - - WidgetsFlutterBinding.ensureInitialized(); + final applicationDataDirectory = dataDirectory ?? + await mockApplicationDataStorage( + pathExtension: pathExtension, + ); await FlowyRunner.run( - FlowyApp(), + AppFlowyApplication(), IntegrationMode.integrationTest, rustEnvsBuilder: () { final rustEnvs = {}; @@ -93,9 +95,10 @@ extension AppFlowyTestBase on WidgetTester { ); }, ); + await waitUntilSignInPageShow(); return FlowyTestContext( - applicationDataDirectory: directory, + applicationDataDirectory: applicationDataDirectory, ); } @@ -110,27 +113,6 @@ extension AppFlowyTestBase on WidgetTester { }); } - Future mockApplicationDataStorage({ - // use to append after the application data directory - String? pathExtension, - }) async { - final dir = await getTemporaryDirectory(); - - // Use a random uuid to avoid conflict. - String path = p.join(dir.path, 'appflowy_integration_test', uuid()); - if (pathExtension != null && pathExtension.isNotEmpty) { - path = '$path/$pathExtension'; - } - final directory = Directory(path); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - - MockApplicationDataStorage.initialPath = directory.path; - - return directory.path; - } - Future waitUntilSignInPageShow() async { if (isAuthEnabled) { final finder = find.byType(SignInAnonymousButton); @@ -143,16 +125,22 @@ extension AppFlowyTestBase on WidgetTester { } } + Future waitForSeconds(int seconds) async { + await Future.delayed((Duration(seconds: seconds)), () {}); + } + Future pumpUntilFound( Finder finder, { Duration timeout = const Duration(seconds: 10), + Duration pumpInterval = + const Duration(milliseconds: 50), // Interval between pumps }) async { bool timerDone = false; final timer = Timer(timeout, () => timerDone = true); - while (timerDone != true) { - await pump(); + while (!timerDone) { + await pump(pumpInterval); // Pump with an interval if (any(finder)) { - timerDone = true; + break; } } timer.cancel(); @@ -273,3 +261,24 @@ Future useAppFlowyCloud() async { await setAuthenticatorType(AuthenticatorType.appflowyCloud); await setAppFlowyCloudUrl(Some(TestEnv.afCloudUrl)); } + +Future mockApplicationDataStorage({ + // use to append after the application data directory + String? pathExtension, +}) async { + final dir = await getTemporaryDirectory(); + + // Use a random uuid to avoid conflict. + String path = p.join(dir.path, 'appflowy_integration_test', uuid()); + if (pathExtension != null && pathExtension.isNotEmpty) { + path = '$path/$pathExtension'; + } + final directory = Directory(path); + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + + MockApplicationDataStorage.initialPath = directory.path; + + return directory.path; +} diff --git a/frontend/appflowy_flutter/integration_test/util/dir.dart b/frontend/appflowy_flutter/integration_test/util/dir.dart index 9df4871222..9dc00a57c9 100644 --- a/frontend/appflowy_flutter/integration_test/util/dir.dart +++ b/frontend/appflowy_flutter/integration_test/util/dir.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:path/path.dart' as p; +import 'package:archive/archive.dart'; Future deleteDirectoriesWithSameBaseNameAsPrefix( String path, @@ -28,3 +29,25 @@ Future deleteDirectoriesWithSameBaseNameAsPrefix( } } } + +Future unzipFile(File zipFile, Directory targetDirectory) async { + // Read the Zip file from disk. + final bytes = zipFile.readAsBytesSync(); + + // Decode the Zip file + final archive = ZipDecoder().decodeBytes(bytes); + + // Extract the contents of the Zip archive to disk. + for (final file in archive) { + final filename = file.name; + if (file.isFile) { + final data = file.content as List; + File(p.join(targetDirectory.path, filename)) + ..createSync(recursive: true) + ..writeAsBytesSync(data); + } else { + Directory(p.join(targetDirectory.path, filename)) + .createSync(recursive: true); + } + } +} diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index f9c5ebe4f5..bbe56e1d59 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -11,14 +11,20 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'util.dart'; + // const String readme = 'Read me'; const String gettingStarted = 'Getting started'; extension Expectation on WidgetTester { /// Expect to see the home page and with a default read me page. - void expectToSeeHomePage() { - expect(find.byType(HomeStack), findsOneWidget); - expect(find.textContaining(gettingStarted), findsWidgets); + Future expectToSeeHomePage() async { + final finder = find.byType(HomeStack); + await pumpUntilFound(finder); + expect(finder, findsOneWidget); + + final docFinder = find.textContaining(gettingStarted); + await pumpUntilFound(docFinder); } /// Expect to see the page name on the home page. diff --git a/frontend/appflowy_flutter/integration_test/util/settings.dart b/frontend/appflowy_flutter/integration_test/util/settings.dart index 68bc4d5d6d..23d9f73d44 100644 --- a/frontend/appflowy_flutter/integration_test/util/settings.dart +++ b/frontend/appflowy_flutter/integration_test/util/settings.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; @@ -13,7 +14,7 @@ import 'base.dart'; extension AppFlowySettings on WidgetTester { /// Open settings page Future openSettings() async { - final settingsButton = find.byTooltip(LocaleKeys.settings_menu_open.tr()); + final settingsButton = find.byType(UserSettingButton); expect(settingsButton, findsOneWidget); await tapButton(settingsButton); final settingsDialog = find.byType(SettingsDialog); diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 28f348d0c5..af7a8abe81 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -131,7 +131,7 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { case AuthenticatorType.local: getIt.registerFactory( () => BackendAuthService( - AuthTypePB.Local, + AuthenticatorPB.Local, ), ); break; diff --git a/frontend/appflowy_flutter/lib/startup/entry_point.dart b/frontend/appflowy_flutter/lib/startup/entry_point.dart index 51350369e5..13987751b3 100644 --- a/frontend/appflowy_flutter/lib/startup/entry_point.dart +++ b/frontend/appflowy_flutter/lib/startup/entry_point.dart @@ -3,7 +3,7 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/splash_screen.dart'; import 'package:flutter/material.dart'; -class FlowyApp implements EntryPoint { +class AppFlowyApplication implements EntryPoint { @override Widget create(LaunchConfiguration config) { return SplashScreen( diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 60a8fe3f7d..1cde15681e 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -29,14 +29,30 @@ class FlowyRunnerContext { FlowyRunnerContext({required this.applicationDataDirectory}); } -Future runAppFlowy() async { - await FlowyRunner.run( - FlowyApp(), - integrationMode(), - ); +Future runAppFlowy({bool isAnon = false}) async { + if (kReleaseMode) { + await FlowyRunner.run( + AppFlowyApplication(), + integrationMode(), + isAnon: isAnon, + ); + } else { + // When running the app in integration test mode, we need to + // specify the mode to run the app again. + await FlowyRunner.run( + AppFlowyApplication(), + FlowyRunner.currentMode, + didInitGetItCallback: IntegrationTestHelper.didInitGetItCallback, + rustEnvsBuilder: IntegrationTestHelper.rustEnvsBuilder, + isAnon: isAnon, + ); + } } class FlowyRunner { + // This variable specifies the initial mode of the app when it is launched for the first time. + // The same mode will be automatically applied in subsequent executions when the runAppFlowy() + // method is called. static var currentMode = integrationMode(); static Future run( @@ -55,6 +71,13 @@ class FlowyRunner { bool isAnon = false, }) async { currentMode = mode; + + // Only set the mode when it's not release mode + if (!kReleaseMode) { + IntegrationTestHelper.didInitGetItCallback = didInitGetItCallback; + IntegrationTestHelper.rustEnvsBuilder = rustEnvsBuilder; + } + // Clear all the states in case of rebuilding. await getIt.reset(); @@ -220,3 +243,9 @@ IntegrationMode integrationMode() { return IntegrationMode.develop; } + +/// Only used for integration test +class IntegrationTestHelper { + static Future Function()? didInitGetItCallback; + static Map Function()? rustEnvsBuilder; +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 67cd67abc9..d781cb6b82 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -87,7 +87,7 @@ class AppFlowyCloudDeepLink { (_) async { final deviceId = await getDeviceId(); final payload = OauthSignInPB( - authType: AuthTypePB.AFCloud, + authType: AuthenticatorPB.AppFlowyCloud, map: { AuthServiceMapKeys.signInURL: uri.toString(), AuthServiceMapKeys.deviceId: deviceId, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index d7d8b7de23..737de56eb7 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -17,7 +17,7 @@ class AppFlowyCloudAuthService implements AuthService { AppFlowyCloudAuthService(); final BackendAuthService _backendAuthService = BackendAuthService( - AuthTypePB.AFCloud, + AuthenticatorPB.AppFlowyCloud, ); @override diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart index 8e860e2ed6..e7d45bd4ed 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart @@ -18,7 +18,7 @@ class AppFlowyCloudMockAuthService implements AuthService { : userEmail = email ?? "${uuid()}@appflowy.io"; final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthTypePB.Supabase); + BackendAuthService(AuthenticatorPB.Supabase); @override Future> signUp({ @@ -45,7 +45,7 @@ class AppFlowyCloudMockAuthService implements AuthService { Map params = const {}, }) async { final payload = SignInUrlPayloadPB.create() - ..authType = AuthTypePB.AFCloud + ..authType = AuthenticatorPB.AppFlowyCloud // don't use nanoid here, the gotrue server will transform the email ..email = userEmail; @@ -55,7 +55,7 @@ class AppFlowyCloudMockAuthService implements AuthService { return getSignInURLResult.fold( (urlPB) async { final payload = OauthSignInPB( - authType: AuthTypePB.AFCloud, + authType: AuthenticatorPB.AppFlowyCloud, map: { AuthServiceMapKeys.signInURL: urlPB.signInUrl, AuthServiceMapKeys.deviceId: deviceId, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index d7ca4ee89f..e990cc75f8 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -14,7 +14,7 @@ import '../../../generated/locale_keys.g.dart'; import 'device_id.dart'; class BackendAuthService implements AuthService { - final AuthTypePB authType; + final AuthenticatorPB authType; BackendAuthService(this.authType); @@ -73,7 +73,7 @@ class BackendAuthService implements AuthService { ..email = userEmail ..password = password // When sign up as guest, the auth type is always local. - ..authType = AuthTypePB.Local + ..authType = AuthenticatorPB.Local ..deviceId = await getDeviceId(); final response = await UserEventSignUp(request).send().then( (value) => value.swap(), @@ -84,7 +84,7 @@ class BackendAuthService implements AuthService { @override Future> signUpWithOAuth({ required String platform, - AuthTypePB authType = AuthTypePB.Local, + AuthenticatorPB authType = AuthenticatorPB.Local, Map params = const {}, }) async { return left( diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart index bdb4ab4752..bb0a22b673 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -22,7 +22,7 @@ class SupabaseAuthService implements AuthService { GoTrueClient get _auth => _client.auth; final BackendAuthService _backendAuthService = BackendAuthService( - AuthTypePB.Supabase, + AuthenticatorPB.Supabase, ); @override @@ -171,7 +171,7 @@ class SupabaseAuthService implements AuthService { required Map map, }) async { final payload = OauthSignInPB( - authType: AuthTypePB.Supabase, + authType: AuthenticatorPB.Supabase, map: map, ); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart index c6863c6954..abfc0910fc 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart @@ -21,7 +21,7 @@ class SupabaseMockAuthService implements AuthService { GoTrueClient get _auth => _client.auth; final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthTypePB.Supabase); + BackendAuthService(AuthenticatorPB.Supabase); @override Future> signUp({ @@ -67,7 +67,7 @@ class SupabaseMockAuthService implements AuthService { // Create the OAuth sign-in payload. final payload = OauthSignInPB( - authType: AuthTypePB.Supabase, + authType: AuthenticatorPB.Supabase, map: { AuthServiceMapKeys.uuid: uuid, AuthServiceMapKeys.email: email, diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index bbd8f40aa5..8f63a70df6 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -72,8 +72,9 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = isSelected || user.authType != AuthTypePB.Local; - final desc = "${user.name}\t ${user.authType}\t"; + final isDisabled = + isSelected || user.authenticator != AuthenticatorPB.Local; + final desc = "${user.name}\t ${user.authenticator}\t"; final child = SizedBox( height: 30, child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart index 0fb70d1820..402704cd2b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart @@ -1,7 +1,6 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/anon_user_bloc.dart'; @@ -101,11 +100,7 @@ class _SkipLogInScreenState extends State { } Future _relaunchAppAndAutoRegister() async { - await FlowyRunner.run( - FlowyApp(), - integrationMode(), - isAnon: true, - ); + await runAppFlowy(isAnon: true); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index b8c76f34c1..31fece1055 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart @@ -46,7 +46,7 @@ class SidebarUser extends StatelessWidget { Expanded( child: _buildUserName(context, state), ), - _buildSettingsButton(context, state), + UserSettingButton(userProfile: state.userProfile), const HSpace(4), NotificationButton(views: views), ], @@ -64,8 +64,22 @@ class SidebarUser extends StatelessWidget { ); } - Widget _buildSettingsButton(BuildContext context, MenuUserState state) { - final userProfile = state.userProfile; + /// Return the user name, if the user name is empty, return the default user name. + String _userName(UserProfilePB userProfile) { + String name = userProfile.name; + if (name.isEmpty) { + name = LocaleKeys.defaultUsername.tr(); + } + return name; + } +} + +class UserSettingButton extends StatelessWidget { + final UserProfilePB userProfile; + const UserSettingButton({required this.userProfile, super.key}); + + @override + Widget build(BuildContext context) { return FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), child: IconButton( @@ -109,13 +123,4 @@ class SidebarUser extends StatelessWidget { ), ); } - - /// Return the user name, if the user name is empty, return the default user name. - String _userName(UserProfilePB userProfile) { - String name = userProfile.name; - if (name.isEmpty) { - name = LocaleKeys.defaultUsername.tr(); - } - return name; - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index 6f7de5ccd9..86c812ee84 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; @@ -207,11 +206,7 @@ class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> { return; } await context.read().setCustomPath(path); - await FlowyRunner.run( - FlowyApp(), - FlowyRunner.currentMode, - isAnon: true, - ); + await runAppFlowy(isAnon: true); if (mounted) { Navigator.of(context).pop(); } @@ -283,11 +278,7 @@ class _RecoverDefaultStorageButtonState await context .read() .resetDataStoragePathToApplicationDefault(); - await FlowyRunner.run( - FlowyApp(), - FlowyRunner.currentMode, - isAnon: true, - ); + await runAppFlowy(isAnon: true); if (mounted) { Navigator.of(context).pop(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index a01f227815..92fb73a147 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -54,7 +54,8 @@ class SettingsUserView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ _buildUserIconSetting(context), - if (isAuthEnabled && user.authType != AuthTypePB.Local) ...[ + if (isAuthEnabled && + user.authenticator != AuthenticatorPB.Local) ...[ const VSpace(12), UserEmailInput(user.email), ], @@ -146,7 +147,7 @@ class SettingsUserView extends StatelessWidget { } // If the user is logged in locally, render a third-party login button. - if (state.userProfile.authType == AuthTypePB.Local) { + if (state.userProfile.authenticator == AuthenticatorPB.Local) { return SettingThirdPartyLogin(didLogin: didLogin); } @@ -284,6 +285,7 @@ class UserNameInputState extends State { @override void dispose() { _controller.dispose(); + _debounce?.cancel(); super.dispose(); } } @@ -348,6 +350,7 @@ class UserEmailInputState extends State { @override void dispose() { _controller.dispose(); + _debounce?.cancel(); super.dispose(); } } diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 9044bf1019..014f45d08a 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -35,7 +35,7 @@ class AppFlowyUnitTest { _pathProviderInitialized(); await FlowyRunner.run( - FlowyTestApp(), + AppFlowyApplicationUniTest(), IntegrationMode.unitTest, ); @@ -111,7 +111,7 @@ void _pathProviderInitialized() { }); } -class FlowyTestApp implements EntryPoint { +class AppFlowyApplicationUniTest implements EntryPoint { @override Widget create(LaunchConfiguration config) { return Container(); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index a258f965c5..71c0269702 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -139,7 +139,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "reqwest", @@ -786,7 +786,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -902,7 +902,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -932,7 +932,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "proc-macro2", "quote", @@ -944,7 +944,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "collab", @@ -963,7 +963,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "bytes", @@ -977,7 +977,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "chrono", @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -1040,7 +1040,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -1066,7 +1066,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "collab", @@ -1471,7 +1471,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -2842,7 +2842,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "futures-util", @@ -2858,7 +2858,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -3280,7 +3280,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "reqwest", @@ -5040,7 +5040,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "bincode", @@ -5062,7 +5062,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "bincode", @@ -5809,7 +5809,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -7699,7 +7699,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 7a83598b44..cbcc539b05 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -57,7 +57,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "5c1a16cec52e628e2fb7648455581d6fe5e84be1" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "80d4048c69a22c0af77b2f2e9b13b1004b2156e2" } # Please use the following script to update collab. # Working directory: frontend # @@ -67,14 +67,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "5c1 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 2b86b72076..b68193cc94 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -125,7 +125,7 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "reqwest", @@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -733,7 +733,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -752,7 +752,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -782,7 +782,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "proc-macro2", "quote", @@ -794,7 +794,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "collab", @@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "bytes", @@ -827,7 +827,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "chrono", @@ -869,7 +869,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -890,7 +890,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "async-trait", @@ -916,7 +916,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0e117f568bd2465762582f6aeb8d9c11fe714e63#0e117f568bd2465762582f6aeb8d9c11fe714e63" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d5324139e6450e32246520416af768202f544869#d5324139e6450e32246520416af768202f544869" dependencies = [ "anyhow", "collab", @@ -1277,7 +1277,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -2483,7 +2483,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "futures-util", @@ -2499,7 +2499,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -2860,7 +2860,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "reqwest", @@ -4329,7 +4329,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "bincode", @@ -4351,7 +4351,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "bincode", @@ -5020,7 +5020,7 @@ dependencies = [ [[package]] name = "shared_entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "app-error", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=5c1a16cec52e628e2fb7648455581d6fe5e84be1#5c1a16cec52e628e2fb7648455581d6fe5e84be1" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=80d4048c69a22c0af77b2f2e9b13b1004b2156e2#80d4048c69a22c0af77b2f2e9b13b1004b2156e2" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 0204a1f1a9..55d0e492d3 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -99,7 +99,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "5c1a16cec52e628e2fb7648455581d6fe5e84be1" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "80d4048c69a22c0af77b2f2e9b13b1004b2156e2" } # Please use the following script to update collab. # Working directory: frontend # @@ -109,11 +109,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "5c1 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0e117f568bd2465762582f6aeb8d9c11fe714e63" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d5324139e6450e32246520416af768202f544869" } diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 7379e52be9..8e2caeb021 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [lib] name = "dart_ffi" # this value will change depending on the target os -# default staticlib +# default static library crate-type = ["staticlib"] diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index a0d315a027..6ac0fb5f4e 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -8,7 +8,7 @@ use parking_lot::RwLock; use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_user::entities::AuthTypePB; +use flowy_user::entities::AuthenticatorPB; use crate::user_event::TestNotificationSender; @@ -21,7 +21,7 @@ pub mod user_event; #[derive(Clone)] pub struct EventIntegrationTest { - pub auth_type: Arc>, + pub auth_type: Arc>, pub inner: AppFlowyCore, #[allow(dead_code)] cleaner: Arc, @@ -48,7 +48,7 @@ impl EventIntegrationTest { let inner = init_core(config).await; let notification_sender = TestNotificationSender::new(); - let auth_type = Arc::new(RwLock::new(AuthTypePB::Local)); + let auth_type = Arc::new(RwLock::new(AuthenticatorPB::Local)); register_notification_sender(notification_sender.clone()); std::mem::forget(inner.dispatcher()); Self { diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index 0c8dfb0003..c133b98954 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -15,7 +15,7 @@ use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_config::af_cloud_config::AFCloudConfiguration; use flowy_server_config::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, CloudSettingPB, OauthSignInPB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, + AuthenticatorPB, CloudSettingPB, OauthSignInPB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; @@ -59,7 +59,7 @@ impl EventIntegrationTest { email, name: "appflowy".to_string(), password: password.clone(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: uuid::Uuid::new_v4().to_string(), } .into_bytes() @@ -88,7 +88,7 @@ impl EventIntegrationTest { let map = third_party_sign_up_param(Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - auth_type: AuthTypePB::Supabase, + auth_type: AuthenticatorPB::Supabase, }; EventBuilder::new(self.clone()) @@ -106,7 +106,7 @@ impl EventIntegrationTest { .await; } - pub fn set_auth_type(&self, auth_type: AuthTypePB) { + pub fn set_auth_type(&self, auth_type: AuthenticatorPB) { *self.auth_type.write() = auth_type; } @@ -133,7 +133,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult { let payload = SignInUrlPayloadPB { email: email.to_string(), - auth_type: AuthTypePB::AFCloud, + auth_type: AuthenticatorPB::AppFlowyCloud, }; let sign_in_url = EventBuilder::new(self.clone()) .event(GenerateSignInURL) @@ -148,7 +148,7 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - auth_type: AuthTypePB::AFCloud, + auth_type: AuthenticatorPB::AppFlowyCloud, }; let user_profile = EventBuilder::new(self.clone()) @@ -175,7 +175,7 @@ impl EventIntegrationTest { ); let payload = OauthSignInPB { map, - auth_type: AuthTypePB::Supabase, + auth_type: AuthenticatorPB::Supabase, }; let user_profile = EventBuilder::new(self.clone()) diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs index 975292da61..9827164f62 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs @@ -1,9 +1,7 @@ -use event_integration::user_event::user_localhost_af_cloud; use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_user::entities::AuthTypePB; -use crate::util::{get_af_cloud_config, unzip_history_user_db}; +use crate::util::unzip_history_user_db; #[tokio::test] async fn reading_039_anon_user_data_test() { @@ -36,70 +34,43 @@ async fn reading_039_anon_user_data_test() { drop(cleaner); } -#[tokio::test] -async fn anon_user_to_af_cloud_test() { - if get_af_cloud_config().is_none() { - return; - } - let (cleaner, user_db_path) = unzip_history_user_db("./tests/asset", "039_local").unwrap(); - user_localhost_af_cloud().await; - let test = - EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; - let anon_first_level_views = test.get_all_workspace_views().await; - let _anon_second_level_views = test - .get_views(&anon_first_level_views[0].id) - .await - .child_views; - - let user = test.af_cloud_sign_up().await; - assert_eq!(user.auth_type, AuthTypePB::AFCloud); - // let mut sync_state = test - // .folder_manager - // .get_mutex_folder() - // .lock() - // .as_ref() - // .unwrap() - // .subscribe_sync_state(); - // - // // TODO(nathan): will be removed when supporting merge FolderData - // // wait until the state is SyncFinished with 10 secs timeout - // loop { - // select! { - // _ = tokio::time::sleep(Duration::from_secs(10)) => { - // panic!("Timeout waiting for sync finished"); - // } - // state = sync_state.next() => { - // if let Some(state) = &state { - // if state == &SyncState::SyncFinished { - // break; - // } - // } - // } - // } - // } - // - // let user_first_level_views = test.get_all_workspace_views().await; - // let user_second_level_views = test - // .get_views(&user_first_level_views[1].id) - // .await - // .child_views; - // - // // first - // assert_eq!(anon_first_level_views.len(), 1); - // assert_eq!(user_first_level_views.len(), 2); - // assert_eq!( - // anon_first_level_views[0].name, - // // The first one is the get started document - // user_first_level_views[1].name - // ); - // assert_ne!(anon_first_level_views[0].id, user_first_level_views[1].id); - // - // // second - // assert_eq!(anon_second_level_views.len(), user_second_level_views.len()); - // assert_eq!( - // anon_second_level_views[0].name, - // user_second_level_views[0].name - // ); - // assert_ne!(anon_second_level_views[0].id, user_second_level_views[0].id); - drop(cleaner); -} +// #[tokio::test] +// async fn migrate_anon_user_data_to_af_cloud_test() { +// let (cleaner, user_db_path) = unzip_history_user_db("./tests/asset", "039_local").unwrap(); +// user_localhost_af_cloud().await; +// let test = +// EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; +// let anon_first_level_views = test.get_all_workspace_views().await; +// let anon_second_level_views = test +// .get_views(&anon_first_level_views[0].id) +// .await +// .child_views; +// +// // The anon user data will be migrated to the AppFlowy cloud after sign up +// let user = test.af_cloud_sign_up().await; +// assert_eq!(user.authenticator, AuthenticatorPB::AppFlowyCloud); +// +// let user_first_level_views = test.get_all_workspace_views().await; +// let user_second_level_views = test +// .get_views(&user_first_level_views[0].id) +// .await +// .child_views; +// +// // first +// assert_eq!(anon_first_level_views.len(), 1); +// assert_eq!(user_first_level_views.len(), 1); +// assert_eq!( +// anon_first_level_views[0].name, +// user_first_level_views[0].name +// ); +// assert_ne!(anon_first_level_views[0].id, user_first_level_views[0].id); +// +// // second +// assert_eq!(anon_second_level_views.len(), user_second_level_views.len()); +// assert_eq!( +// anon_second_level_views[0].name, +// user_second_level_views[0].name +// ); +// assert_ne!(anon_second_level_views[0].id, user_second_level_views[0].id); +// drop(cleaner); +// } diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs index bb864cc3b0..010a88d484 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/auth_test.rs @@ -1,6 +1,6 @@ use event_integration::user_event::{login_password, unique_email}; use event_integration::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB}; +use flowy_user::entities::{AuthenticatorPB, SignInPayloadPB, SignUpPayloadPB}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -14,7 +14,7 @@ async fn sign_up_with_invalid_email() { email: email.to_string(), name: valid_name(), password: login_password(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -38,7 +38,7 @@ async fn sign_up_with_long_password() { email: unique_email(), name: valid_name(), password: "1234".repeat(100).as_str().to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -63,7 +63,7 @@ async fn sign_in_with_invalid_email() { email: email.to_string(), password: login_password(), name: "".to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -90,7 +90,7 @@ async fn sign_in_with_invalid_password() { email: unique_email(), password, name: "".to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs index 7418b267af..17274059b3 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs @@ -1,7 +1,7 @@ use nanoid::nanoid; use event_integration::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; use crate::user::local_test::helper::*; @@ -32,7 +32,7 @@ async fn anon_user_profile_get() { assert_eq!(user_profile.openai_key, user.openai_key); assert_eq!(user_profile.stability_ai_key, user.stability_ai_key); assert_eq!(user_profile.workspace_id, user.workspace_id); - assert_eq!(user_profile.auth_type, AuthTypePB::Local); + assert_eq!(user_profile.authenticator, AuthenticatorPB::Local); } #[tokio::test] diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs index 53a0d71179..5f5479a86b 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs @@ -14,7 +14,9 @@ use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_encrypt::decrypt_text; use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; -use flowy_user::entities::{AuthTypePB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB}; +use flowy_user::entities::{ + AuthenticatorPB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB, +}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -33,7 +35,7 @@ async fn third_party_sign_up_test() { map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - auth_type: AuthTypePB::Supabase, + auth_type: AuthenticatorPB::Supabase, }; let response = EventBuilder::new(test.clone()) @@ -77,7 +79,7 @@ async fn third_party_sign_up_with_duplicated_uuid() { .event(OauthSignIn) .payload(OauthSignInPB { map: map.clone(), - auth_type: AuthTypePB::Supabase, + auth_type: AuthenticatorPB::Supabase, }) .async_send() .await @@ -88,7 +90,7 @@ async fn third_party_sign_up_with_duplicated_uuid() { .event(OauthSignIn) .payload(OauthSignInPB { map: map.clone(), - auth_type: AuthTypePB::Supabase, + auth_type: AuthenticatorPB::Supabase, }) .async_send() .await diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs index 543e76f262..ccd1d6fb03 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/workspace_test.rs @@ -4,7 +4,7 @@ use event_integration::{event_builder::EventBuilder, EventIntegrationTest}; use flowy_folder2::entities::WorkspaceSettingPB; use flowy_folder2::event_map::FolderEvent::GetCurrentWorkspaceSetting; use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; -use flowy_user::entities::{AuthTypePB, OauthSignInPB, UserProfilePB}; +use flowy_user::entities::{AuthenticatorPB, OauthSignInPB, UserProfilePB}; use flowy_user::event_map::UserEvent::*; use crate::util::*; @@ -21,7 +21,7 @@ async fn initial_workspace_test() { ); let payload = OauthSignInPB { map, - auth_type: AuthTypePB::Supabase, + auth_type: AuthenticatorPB::Supabase, }; let _ = EventBuilder::new(test.clone()) diff --git a/frontend/rust-lib/event-integration/tests/util.rs b/frontend/rust-lib/event-integration/tests/util.rs index c62b58360c..0491fe4851 100644 --- a/frontend/rust-lib/event-integration/tests/util.rs +++ b/frontend/rust-lib/event-integration/tests/util.rs @@ -23,7 +23,7 @@ use flowy_server::supabase::api::*; use flowy_server::{AppFlowyEncryption, EncryptionImpl}; use flowy_server_config::af_cloud_config::AFCloudConfiguration; use flowy_server_config::supabase_config::SupabaseConfiguration; -use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB}; +use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB}; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserCloudServiceProvider; use flowy_user::event_map::UserEvent::*; @@ -43,7 +43,7 @@ impl FlowySupabaseTest { pub async fn new() -> Option { let _ = get_supabase_config()?; let test = EventIntegrationTest::new().await; - test.set_auth_type(AuthTypePB::Supabase); + test.set_auth_type(AuthenticatorPB::Supabase); test .server_provider .set_authenticator(Authenticator::Supabase); @@ -210,7 +210,7 @@ impl AFCloudTest { pub async fn new() -> Option { let _ = get_af_cloud_config()?; let test = EventIntegrationTest::new().await; - test.set_auth_type(AuthTypePB::AFCloud); + test.set_auth_type(AuthenticatorPB::AppFlowyCloud); test .server_provider .set_authenticator(Authenticator::AppFlowyCloud); diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 8c1f8c00aa..8b1ae63866 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -22,25 +22,25 @@ pub(crate) const SERVER_PROVIDER_TYPE_KEY: &str = "server_provider_type"; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] #[repr(u8)] -pub enum ServerType { +pub enum Server { /// Local server provider. /// Offline mode, no user authentication and the data is stored locally. Local = 0, /// AppFlowy Cloud server provider. /// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Cloud) is still a work in /// progress. - AFCloud = 1, + AppFlowyCloud = 1, /// Supabase server provider. /// It uses supabase postgresql database to store data and user authentication. Supabase = 2, } -impl Display for ServerType { +impl Display for Server { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - ServerType::Local => write!(f, "Local"), - ServerType::AFCloud => write!(f, "AppFlowyCloud"), - ServerType::Supabase => write!(f, "Supabase"), + Server::Local => write!(f, "Local"), + Server::AppFlowyCloud => write!(f, "AppFlowyCloud"), + Server::Supabase => write!(f, "Supabase"), } } } @@ -51,8 +51,8 @@ impl Display for ServerType { /// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. pub struct ServerProvider { config: AppFlowyCoreConfig, - server_type: RwLock, - providers: RwLock>>, + server: RwLock, + providers: RwLock>>, pub(crate) encryption: RwLock>, pub(crate) store_preferences: Weak, pub(crate) enable_sync: RwLock, @@ -62,13 +62,13 @@ pub struct ServerProvider { impl ServerProvider { pub fn new( config: AppFlowyCoreConfig, - server_type: ServerType, + server: Server, store_preferences: Weak, ) -> Self { let encryption = EncryptionImpl::new(None); Self { config, - server_type: RwLock::new(server_type), + server: RwLock::new(server), providers: RwLock::new(HashMap::new()), enable_sync: RwLock::new(true), encryption: RwLock::new(Arc::new(encryption)), @@ -77,37 +77,34 @@ impl ServerProvider { } } - pub fn get_server_type(&self) -> ServerType { - self.server_type.read().clone() + pub fn get_server_type(&self) -> Server { + self.server.read().clone() } - pub fn set_server_type(&self, server_type: ServerType) { - let old_server_type = self.server_type.read().clone(); + pub fn set_server_type(&self, server_type: Server) { + let old_server_type = self.server.read().clone(); if server_type != old_server_type { self.providers.write().remove(&old_server_type); } - *self.server_type.write() = server_type; + *self.server.write() = server_type; } /// Returns a [AppFlowyServer] trait implementation base on the provider_type. - pub(crate) fn get_server( - &self, - server_type: &ServerType, - ) -> FlowyResult> { + pub(crate) fn get_server(&self, server_type: &Server) -> FlowyResult> { if let Some(provider) = self.providers.read().get(server_type) { return Ok(provider.clone()); } let server = match server_type { - ServerType::Local => { + Server::Local => { let local_db = Arc::new(LocalServerDBImpl { storage_path: self.config.storage_path.clone(), }); let server = Arc::new(LocalServer::new(local_db)); Ok::, FlowyError>(server) }, - ServerType::AFCloud => { + Server::AppFlowyCloud => { let config = AFCloudConfiguration::from_env()?; let server = Arc::new(AppFlowyCloudServer::new( config, @@ -117,7 +114,7 @@ impl ServerProvider { Ok::, FlowyError>(server) }, - ServerType::Supabase => { + Server::Supabase => { let config = SupabaseConfiguration::from_env()?; let uid = self.uid.clone(); tracing::trace!("🔑Supabase config: {:?}", config); @@ -140,36 +137,35 @@ impl ServerProvider { } } -impl From for ServerType { +impl From for Server { fn from(auth_provider: Authenticator) -> Self { match auth_provider { - Authenticator::Local => ServerType::Local, - Authenticator::AppFlowyCloud => ServerType::AFCloud, - Authenticator::Supabase => ServerType::Supabase, + Authenticator::Local => Server::Local, + Authenticator::AppFlowyCloud => Server::AppFlowyCloud, + Authenticator::Supabase => Server::Supabase, } } } -impl From for Authenticator { - fn from(ty: ServerType) -> Self { +impl From for Authenticator { + fn from(ty: Server) -> Self { match ty { - ServerType::Local => Authenticator::Local, - ServerType::AFCloud => Authenticator::AppFlowyCloud, - ServerType::Supabase => Authenticator::Supabase, + Server::Local => Authenticator::Local, + Server::AppFlowyCloud => Authenticator::AppFlowyCloud, + Server::Supabase => Authenticator::Supabase, } } } -impl From<&Authenticator> for ServerType { +impl From<&Authenticator> for Server { fn from(auth_provider: &Authenticator) -> Self { Self::from(auth_provider.clone()) } } -pub fn current_server_type(store_preferences: &Arc) -> ServerType { - match store_preferences.get_object::(SERVER_PROVIDER_TYPE_KEY) { - None => ServerType::Local, - Some(provider_type) => provider_type, - } +pub fn current_server_type(store_preferences: &Arc) -> Server { + store_preferences + .get_object::(SERVER_PROVIDER_TYPE_KEY) + .unwrap_or(Server::Local) } struct LocalServerDBImpl { diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 26607a5b67..f3ff03a328 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -30,7 +30,7 @@ use flowy_user_deps::cloud::UserCloudService; use flowy_user_deps::entities::{Authenticator, UserTokenState}; use lib_infra::future::{to_fut, Fut, FutureResult}; -use crate::integrate::server::{ServerProvider, ServerType, SERVER_PROVIDER_TYPE_KEY}; +use crate::integrate::server::{Server, ServerProvider, SERVER_PROVIDER_TYPE_KEY}; impl FileStorageService for ServerProvider { fn create_object(&self, object: StorageObject) -> FutureResult { @@ -91,12 +91,12 @@ impl UserCloudServiceProvider for ServerProvider { /// When user login, the provider type is set by the [Authenticator] and save to disk for next use. /// - /// Each [Authenticator] has a corresponding [ServerType]. The [ServerType] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerType] is set, + /// Each [Authenticator] has a corresponding [Server]. The [Server] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [Server] is set, /// it will be used when user open the app again. /// fn set_authenticator(&self, authenticator: Authenticator) { - let server_type: ServerType = authenticator.into(); + let server_type: Server = authenticator.into(); self.set_server_type(server_type.clone()); match self.store_preferences.upgrade() { @@ -117,7 +117,7 @@ impl UserCloudServiceProvider for ServerProvider { Authenticator::from(server_type) } - /// Returns the [UserCloudService] base on the current [ServerType]. + /// Returns the [UserCloudService] base on the current [Server]. /// Creates a new [AppFlowyServer] if it doesn't exist. fn get_user_service(&self) -> Result, FlowyError> { let server_type = self.get_server_type(); @@ -127,11 +127,11 @@ impl UserCloudServiceProvider for ServerProvider { fn service_url(&self) -> String { match self.get_server_type() { - ServerType::Local => "".to_string(), - ServerType::AFCloud => AFCloudConfiguration::from_env() + Server::Local => "".to_string(), + Server::AppFlowyCloud => AFCloudConfiguration::from_env() .map(|config| config.base_url) .unwrap_or_default(), - ServerType::Supabase => SupabaseConfiguration::from_env() + Server::Supabase => SupabaseConfiguration::from_env() .map(|config| config.url) .unwrap_or_default(), } @@ -325,7 +325,7 @@ impl CollabStorageProvider for ServerProvider { collab_object, local_collab, } => { - if let Ok(server) = self.get_server(&ServerType::AFCloud) { + if let Ok(server) = self.get_server(&Server::AppFlowyCloud) { to_fut(async move { let mut plugins: Vec> = vec![]; match server.collab_ws_channel(&collab_object.object_id).await { @@ -373,7 +373,7 @@ impl CollabStorageProvider for ServerProvider { } => { let mut plugins: Vec> = vec![]; if let Some(remote_collab_storage) = self - .get_server(&ServerType::Supabase) + .get_server(&Server::Supabase) .ok() .and_then(|provider| provider.collab_storage(&collab_object)) { diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index c9b5b68d2f..2563fbc0d2 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -26,7 +26,7 @@ use crate::deps_resolve::collab_backup::RocksdbBackupImpl; use crate::deps_resolve::*; use crate::integrate::collab_interact::CollabInteractImpl; use crate::integrate::log::init_log; -use crate::integrate::server::{current_server_type, ServerProvider, ServerType}; +use crate::integrate::server::{current_server_type, Server, ServerProvider}; use crate::integrate::user::UserStatusCallbackImpl; pub mod config; @@ -232,12 +232,12 @@ fn init_user_manager( ) } -impl From for CollabDataSource { - fn from(server_type: ServerType) -> Self { +impl From for CollabDataSource { + fn from(server_type: Server) -> Self { match server_type { - ServerType::Local => CollabDataSource::Local, - ServerType::AFCloud => CollabDataSource::AppFlowyCloud, - ServerType::Supabase => CollabDataSource::Supabase, + Server::Local => CollabDataSource::Local, + Server::AppFlowyCloud => CollabDataSource::AppFlowyCloud, + Server::Supabase => CollabDataSource::Supabase, } } } diff --git a/frontend/rust-lib/flowy-document2/src/parser/utils.rs b/frontend/rust-lib/flowy-document2/src/parser/utils.rs index 59719d7311..e5365f2227 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/utils.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/utils.rs @@ -5,7 +5,6 @@ use collab_document::blocks::DocumentData; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; -use tracing::trace; use validator::ValidationError; pub fn get_delta_for_block(block_id: &str, data: &DocumentData) -> Option> { diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 372f6951b1..e42c1198a5 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -47,9 +47,6 @@ pub struct FolderManager { pub cloud_service: Arc, } -unsafe impl Send for FolderManager {} -unsafe impl Sync for FolderManager {} - impl FolderManager { pub async fn new( user: Arc, @@ -1012,8 +1009,8 @@ impl FolderManager { /// Return the views that belong to the workspace. The views are filtered by the trash. pub(crate) fn get_workspace_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { - let trash_ids = folder - .get_all_trash() + let items = folder.get_all_trash(); + let trash_ids = items .into_iter() .map(|trash| trash.id) .collect::>(); diff --git a/frontend/rust-lib/flowy-folder2/src/manager_init.rs b/frontend/rust-lib/flowy-folder2/src/manager_init.rs index f7d61a4306..55f3c52506 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager_init.rs @@ -37,10 +37,10 @@ impl FolderManager { let collab_db = self.user.collab_db(uid)?; let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); - let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); + let (section_change_tx, section_change_rx) = tokio::sync::broadcast::channel(100); let folder_notifier = FolderNotify { view_change_tx: view_tx, - trash_change_tx: trash_tx, + section_change_tx, }; let folder = match initial_data { @@ -99,7 +99,7 @@ impl FolderManager { let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); subscribe_folder_sync_state_changed(workspace_id.clone(), folder_state_rx, &weak_mutex_folder); subscribe_folder_snapshot_state_changed(workspace_id, &weak_mutex_folder); - subscribe_folder_trash_changed(trash_rx, &weak_mutex_folder); + subscribe_folder_trash_changed(section_change_rx, &weak_mutex_folder); subscribe_folder_view_changed(view_rx, &weak_mutex_folder); Ok(()) } diff --git a/frontend/rust-lib/flowy-folder2/src/manager_observer.rs b/frontend/rust-lib/flowy-folder2/src/manager_observer.rs index ed18cf25ab..6209aadcc4 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager_observer.rs @@ -3,7 +3,8 @@ use std::sync::{Arc, Weak}; use collab::core::collab_state::SyncState; use collab_folder::{ - Folder, TrashChange, TrashChangeReceiver, View, ViewChange, ViewChangeReceiver, + Folder, SectionChange, SectionChangeReceiver, TrashSectionChange, View, ViewChange, + ViewChangeReceiver, }; use tokio_stream::wrappers::WatchStream; use tokio_stream::StreamExt; @@ -102,7 +103,7 @@ pub(crate) fn subscribe_folder_sync_state_changed( /// Listen on the [TrashChange]s and notify the frontend some views were changed. pub(crate) fn subscribe_folder_trash_changed( - mut rx: TrashChangeReceiver, + mut rx: SectionChangeReceiver, weak_mutex_folder: &Weak, ) { let weak_mutex_folder = weak_mutex_folder.clone(); @@ -111,25 +112,29 @@ pub(crate) fn subscribe_folder_trash_changed( if let Some(folder) = weak_mutex_folder.upgrade() { let mut unique_ids = HashSet::new(); tracing::trace!("Did receive trash change: {:?}", value); - let ids = match value { - TrashChange::DidCreateTrash { ids } => ids, - TrashChange::DidDeleteTrash { ids } => ids, - }; - if let Some(folder) = folder.lock().as_ref() { - let views = folder.views.get_views(&ids); - for view in views { - unique_ids.insert(view.parent_view_id.clone()); - } + match value { + SectionChange::Trash(change) => { + let ids = match change { + TrashSectionChange::TrashItemAdded { ids } => ids, + TrashSectionChange::TrashItemRemoved { ids } => ids, + }; + if let Some(folder) = folder.lock().as_ref() { + let views = folder.views.get_views(&ids); + for view in views { + unique_ids.insert(view.parent_view_id.clone()); + } - let repeated_trash: RepeatedTrashPB = folder.get_all_trash().into(); - send_notification("trash", FolderNotification::DidUpdateTrash) - .payload(repeated_trash) - .send(); + let repeated_trash: RepeatedTrashPB = folder.get_all_trash().into(); + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(repeated_trash) + .send(); + } + + let parent_view_ids = unique_ids.into_iter().collect(); + notify_parent_view_did_change(folder.clone(), parent_view_ids); + }, } - - let parent_view_ids = unique_ids.into_iter().collect(); - notify_parent_view_did_change(folder.clone(), parent_view_ids); } } }); diff --git a/frontend/rust-lib/flowy-folder2/src/user_default.rs b/frontend/rust-lib/flowy-folder2/src/user_default.rs index 910273a60b..128278c347 100644 --- a/frontend/rust-lib/flowy-folder2/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder2/src/user_default.rs @@ -54,6 +54,7 @@ impl DefaultFolderBuilder { views: FlattedViews::flatten_views(views), favorites: Default::default(), recent: Default::default(), + trash: Default::default(), } } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index 1a794f212f..9f7b94ca44 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,6 +1,6 @@ use anyhow::Error; use client_api::entity::QueryCollabResult::{Failed, Success}; -use client_api::entity::{BatchQueryCollab, BatchQueryCollabParams, QueryCollabParams}; +use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; use collab::core::collab_plugin::EncodedCollabV1; use collab_entity::CollabType; @@ -31,8 +31,10 @@ where FutureResult::new(async move { let params = QueryCollabParams { workspace_id, - object_id, - collab_type, + inner: QueryCollab { + object_id, + collab_type, + }, }; match try_get_client?.get_collab(params).await { Ok(data) => Ok(vec![data.doc_state.to_vec()]), @@ -57,15 +59,13 @@ where let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let client = try_get_client?; - let params = BatchQueryCollabParams( - object_ids - .into_iter() - .map(|object_id| BatchQueryCollab { - object_id, - collab_type: object_ty.clone(), - }) - .collect(), - ); + let params = object_ids + .into_iter() + .map(|object_id| QueryCollab { + object_id, + collab_type: object_ty.clone(), + }) + .collect(); let results = client.batch_get_collab(&workspace_id, params).await?; Ok( results diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 3541b11b4b..23525aec10 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -1,5 +1,5 @@ use anyhow::Error; -use client_api::entity::QueryCollabParams; +use client_api::entity::{QueryCollab, QueryCollabParams}; use collab::core::origin::CollabOrigin; use collab_document::document::Document; use collab_entity::CollabType; @@ -27,8 +27,10 @@ where FutureResult::new(async move { let params = QueryCollabParams { workspace_id, - object_id: document_id.to_string(), - collab_type: CollabType::Document, + inner: QueryCollab { + object_id: document_id.to_string(), + collab_type: CollabType::Document, + }, }; let data = try_get_client? .get_collab(params) @@ -60,8 +62,10 @@ where FutureResult::new(async move { let params = QueryCollabParams { workspace_id, - object_id: document_id.clone(), - collab_type: CollabType::Document, + inner: QueryCollab { + object_id: document_id.clone(), + collab_type: CollabType::Document, + }, }; let doc_state = try_get_client? .get_collab(params) diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 23d88cfe48..335575e209 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Error}; -use client_api::entity::QueryCollabParams; +use client_api::entity::{QueryCollab, QueryCollabParams}; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; @@ -60,9 +60,11 @@ where let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let params = QueryCollabParams { - object_id: workspace_id.clone(), workspace_id: workspace_id.clone(), - collab_type: CollabType::Folder, + inner: QueryCollab { + object_id: workspace_id.clone(), + collab_type: CollabType::Folder, + }, }; let doc_state = try_get_client? .get_collab(params) @@ -98,9 +100,11 @@ where let try_get_client = self.0.try_get_client(); FutureResult::new(async move { let params = QueryCollabParams { - object_id: workspace_id.clone(), - workspace_id, - collab_type: CollabType::Folder, + workspace_id: workspace_id.clone(), + inner: QueryCollab { + object_id: workspace_id, + collab_type: CollabType::Folder, + }, }; let doc_state = try_get_client? .get_collab(params) diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index cfaa609e4d..91212aad04 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; use client_api::entity::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset}; -use client_api::entity::{AFRole, AFWorkspace, AuthProvider, InsertCollabParams}; +use client_api::entity::{AFRole, AFWorkspace, AuthProvider, CollabParams, CreateCollabParams}; use collab_entity::CollabObject; use parking_lot::RwLock; @@ -231,16 +231,20 @@ where &self, collab_object: &CollabObject, data: Vec, + override_if_exist: bool, ) -> FutureResult<(), Error> { let try_get_client = self.server.try_get_client(); let collab_object = collab_object.clone(); FutureResult::new(async move { let client = try_get_client?; - let params = InsertCollabParams::new( - collab_object.object_id.clone(), - collab_object.collab_type.clone(), - data, + let params = CreateCollabParams::new( collab_object.workspace_id.clone(), + CollabParams { + object_id: collab_object.object_id.clone(), + encoded_collab_v1: data, + collab_type: collab_object.collab_type.clone(), + override_if_exist, + }, ); client.create_collab(params).await?; Ok(()) diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index ddf0176eae..16a23170cf 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -141,6 +141,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { &self, _collab_object: &CollabObject, _data: Vec, + _override_if_exist: bool, ) -> FutureResult<(), Error> { FutureResult::new(async { Ok(()) }) } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 650fb6b997..8e3558824e 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -98,12 +98,10 @@ where // Query the user profile and workspaces tracing::debug!("user uuid: {}", params.uuid); - let user_profile = get_user_profile( - postgrest.clone(), - GetUserProfileParams::Uuid(params.uuid.clone()), - ) - .await? - .unwrap(); + let user_profile = + get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(params.uuid)) + .await? + .unwrap(); let user_workspaces = get_user_workspaces(postgrest.clone(), user_profile.uid).await?; let latest_workspace = user_workspaces .iter() @@ -137,7 +135,7 @@ where FutureResult::new(async move { let postgrest = try_get_postgrest?; let params = oauth_params_from_box_any(params)?; - let uuid = params.uuid.clone(); + let uuid = params.uuid; let response = get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(uuid)) .await? .unwrap(); @@ -311,7 +309,8 @@ where fn create_collab_object( &self, collab_object: &CollabObject, - update: Vec, + data: Vec, + _override_if_exist: bool, ) -> FutureResult<(), Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let cloned_collab_object = collab_object.clone(); @@ -319,7 +318,7 @@ where af_spawn(async move { tx.send( async move { - CreateCollabAction::new(cloned_collab_object, try_get_postgrest?, update) + CreateCollabAction::new(cloned_collab_object, try_get_postgrest?, data) .run() .await?; Ok(()) diff --git a/frontend/rust-lib/flowy-user-deps/src/cloud.rs b/frontend/rust-lib/flowy-user-deps/src/cloud.rs index 03141af05b..b21f704e4b 100644 --- a/frontend/rust-lib/flowy-user-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-deps/src/cloud.rs @@ -141,6 +141,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, collab_object: &CollabObject, data: Vec, + override_if_exist: bool, ) -> FutureResult<(), Error>; } diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/migrate_anon_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/migrate_anon_user_collab.rs index ee0a3dbf59..83862c875d 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/migrate_anon_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/migrate_anon_user_collab.rs @@ -17,6 +17,7 @@ use parking_lot::{Mutex, RwLock}; use collab_integrate::{PersistenceError, RocksCollabDB, YrsDocAction}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder_deps::cloud::gen_view_id; +use flowy_user_deps::entities::Authenticator; use crate::migrations::MigrationUser; @@ -27,6 +28,7 @@ pub fn migration_anon_user_on_sign_up( old_collab_db: &Arc, new_user: &MigrationUser, new_collab_db: &Arc, + authenticator: &Authenticator, ) -> FlowyResult<()> { new_collab_db .with_write_txn(|new_collab_w_txn| { @@ -72,6 +74,7 @@ pub fn migration_anon_user_on_sign_up( &old_collab_r_txn, new_user, new_collab_w_txn, + authenticator, )?; // Migrate other collab objects @@ -191,6 +194,7 @@ fn migrate_workspace_folder<'a, 'b, W>( old_collab_r_txn: &'b W, new_user: &MigrationUser, new_collab_w_txn: &'a W, + _authenticator: &Authenticator, ) -> Result<(), PersistenceError> where 'a: 'b, @@ -206,9 +210,9 @@ where old_folder_collab.with_origin_transact_mut(|txn| { old_collab_r_txn.load_doc_with_txn(old_uid, old_workspace_id, txn) })?; - let oid_user_id = UserId::from(old_uid); + let old_user_id = UserId::from(old_uid); let old_folder = Folder::open( - oid_user_id, + old_user_id.clone(), Arc::new(MutexCollab::from_collab(old_folder_collab)), None, ) @@ -219,6 +223,41 @@ where "Can't migrate the folder data" )))?; + if let Some(old_fav_map) = folder_data.favorites.remove(&old_user_id) { + let fav_map = old_fav_map + .into_iter() + .map(|mut item| { + let new_view_id = old_to_new_id_map.get_new_id(&item.id); + item.id = new_view_id; + item + }) + .collect(); + folder_data.favorites.insert(UserId::from(new_uid), fav_map); + } + if let Some(old_trash_map) = folder_data.trash.remove(&old_user_id) { + let trash_map = old_trash_map + .into_iter() + .map(|mut item| { + let new_view_id = old_to_new_id_map.get_new_id(&item.id); + item.id = new_view_id; + item + }) + .collect(); + folder_data.trash.insert(UserId::from(new_uid), trash_map); + } + + if let Some(old_recent_map) = folder_data.recent.remove(&old_user_id) { + let recent_map = old_recent_map + .into_iter() + .map(|mut item| { + let new_view_id = old_to_new_id_map.get_new_id(&item.id); + item.id = new_view_id; + item + }) + .collect(); + folder_data.recent.insert(UserId::from(new_uid), recent_map); + } + old_to_new_id_map .0 .insert(old_workspace_id.to_string(), new_workspace_id.to_string()); diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_af_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_af_user_collab.rs index 88f934cdd7..0705667c85 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_af_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_af_user_collab.rs @@ -109,7 +109,7 @@ fn sync_view( encode_v1.len() ); user_service - .create_collab_object(&collab_object, encode_v1) + .create_collab_object(&collab_object, encode_v1, false) .await?; }, ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { @@ -121,7 +121,7 @@ fn sync_view( database_encode_v1.len() ); user_service - .create_collab_object(&collab_object, database_encode_v1) + .create_collab_object(&collab_object, database_encode_v1, false) .await?; // sync database's row @@ -145,7 +145,7 @@ fn sync_view( ); let _ = user_service - .create_collab_object(&database_row_collab_object, database_row_encode_v1) + .create_collab_object(&database_row_collab_object, database_row_encode_v1, false) .await; let database_row_document = CollabObject::new( @@ -165,7 +165,7 @@ fn sync_view( document_encode_v1.len() ); let _ = user_service - .create_collab_object(&database_row_document, document_encode_v1) + .create_collab_object(&database_row_document, document_encode_v1, false) .await; } } @@ -271,7 +271,7 @@ async fn sync_folder( encode_v1.len() ); if let Err(err) = user_service - .create_collab_object(&collab_object, encode_v1) + .create_collab_object(&collab_object, encode_v1, true) .await { tracing::error!("🔴sync folder failed: {:?}", err); @@ -316,7 +316,7 @@ async fn sync_database_views( if let Ok((records, encode_v1)) = result { if let Ok(encode_v1) = encode_v1 { let _ = user_service - .create_collab_object(&collab_object, encode_v1) + .create_collab_object(&collab_object, encode_v1, false) .await; } diff --git a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_supabase_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_supabase_user_collab.rs index 63da725f4e..a1cc335402 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_supabase_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user_upgrade/sync_supabase_user_collab.rs @@ -109,7 +109,7 @@ fn sync_view( doc_state.len() ); user_service - .create_collab_object(&collab_object, doc_state) + .create_collab_object(&collab_object, doc_state, false) .await?; }, ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { @@ -121,7 +121,7 @@ fn sync_view( database_doc_state.len() ); user_service - .create_collab_object(&collab_object, database_doc_state) + .create_collab_object(&collab_object, database_doc_state, false) .await?; // sync database's row @@ -145,7 +145,7 @@ fn sync_view( ); let _ = user_service - .create_collab_object(&database_row_collab_object, database_row_doc_state) + .create_collab_object(&database_row_collab_object, database_row_doc_state, false) .await; let database_row_document = CollabObject::new( @@ -165,7 +165,7 @@ fn sync_view( document_doc_state.len() ); let _ = user_service - .create_collab_object(&database_row_document, document_doc_state) + .create_collab_object(&database_row_document, document_doc_state, false) .await; } } @@ -281,7 +281,7 @@ async fn sync_folder( update.len() ); if let Err(err) = user_service - .create_collab_object(&collab_object, update.to_vec()) + .create_collab_object(&collab_object, update.to_vec(), false) .await { tracing::error!("🔴sync folder failed: {:?}", err); @@ -325,7 +325,7 @@ async fn sync_database_views( if let Ok((records, doc_state)) = result { let _ = user_service - .create_collab_object(&collab_object, doc_state.to_vec()) + .create_collab_object(&collab_object, doc_state.to_vec(), false) .await; records.into_iter().map(Arc::new).collect() } else { diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 99a7fe9960..1e8332c462 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -19,7 +19,7 @@ pub struct SignInPayloadPB { pub name: String, #[pb(index = 4)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, #[pb(index = 5)] pub device_id: String, @@ -53,7 +53,7 @@ pub struct SignUpPayloadPB { pub password: String, #[pb(index = 4)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, #[pb(index = 5)] pub device_id: String, @@ -88,7 +88,7 @@ pub struct OauthSignInPB { pub map: HashMap, #[pb(index = 2)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf, Default)] @@ -97,7 +97,7 @@ pub struct SignInUrlPayloadPB { pub email: String, #[pb(index = 2)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf, Default)] @@ -173,13 +173,13 @@ pub struct OauthProviderDataPB { } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] -pub enum AuthTypePB { +pub enum AuthenticatorPB { Local = 0, Supabase = 1, - AFCloud = 2, + AppFlowyCloud = 2, } -impl Default for AuthTypePB { +impl Default for AuthenticatorPB { fn default() -> Self { Self::Local } @@ -232,7 +232,7 @@ impl From for UserCredentials { #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf, Debug, Default, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 186dc5a6d9..81f2272cd6 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -7,7 +7,7 @@ use flowy_user_deps::entities::*; use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; use crate::entities::required_not_empty_str; -use crate::entities::AuthTypePB; +use crate::entities::AuthenticatorPB; use crate::errors::ErrorCode; use super::parser::UserStabilityAIKey; @@ -45,7 +45,7 @@ pub struct UserProfilePB { pub openai_key: String, #[pb(index = 7)] - pub auth_type: AuthTypePB, + pub authenticator: AuthenticatorPB, #[pb(index = 8)] pub encryption_sign: String, @@ -85,7 +85,7 @@ impl std::convert::From for UserProfilePB { token: user_profile.token, icon_url: user_profile.icon_url, openai_key: user_profile.openai_key, - auth_type: user_profile.authenticator.into(), + authenticator: user_profile.authenticator.into(), encryption_sign, encryption_type: encryption_ty, workspace_id: user_profile.workspace_id, diff --git a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs index 7be2764675..95f0d83e6c 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs @@ -114,7 +114,7 @@ impl std::default::Default for LocaleSettingsPB { } } -#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)] +#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone, Default)] pub struct DocumentSettingsPB { #[pb(index = 1, one_of)] pub cursor_color: Option, @@ -123,15 +123,6 @@ pub struct DocumentSettingsPB { pub selection_color: Option, } -impl std::default::Default for DocumentSettingsPB { - fn default() -> Self { - Self { - cursor_color: None, - selection_color: None, - } - } -} - pub const APPEARANCE_DEFAULT_THEME: &str = "Default"; pub const APPEARANCE_DEFAULT_FONT: &str = "Poppins"; pub const APPEARANCE_DEFAULT_MONOSPACE_FONT: &str = "SF Mono"; diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index 6007a73541..688184dbf9 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -35,6 +35,7 @@ use crate::migrations::document_empty_content::HistoricalEmptyDocumentMigration; use crate::migrations::migration::{UserDataMigration, UserLocalDataMigration}; use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::migrations::workspace_and_favorite_v1::FavoriteV1AndWorkspaceArrayMigration; +use crate::migrations::workspace_trash_v1::WorkspaceTrashMapToSectionMigration; use crate::migrations::MigrationUser; use crate::services::cloud_config::get_cloud_config; use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract}; @@ -198,16 +199,18 @@ impl UserManager { let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { event!(tracing::Level::DEBUG, "Listen token state change"); + let user_uid = user.uid; + let user_token = user.token.clone(); af_spawn(async move { while let Some(token_state) = token_state_rx.next().await { debug!("Token state changed: {:?}", token_state); match token_state { UserTokenState::Refresh { token } => { // Only save the token if the token is different from the current token - if token != user.token { + if token != user_token { if let Some(pool) = weak_pool.upgrade() { // Save the new token - if let Err(err) = save_user_token(user.uid, pool, token) { + if let Err(err) = save_user_token(user_uid, pool, token) { error!("Save user token failed: {}", err); } } @@ -232,9 +235,10 @@ impl UserManager { let migrations: Vec> = vec![ Box::new(HistoricalEmptyDocumentMigration), Box::new(FavoriteV1AndWorkspaceArrayMigration), + Box::new(WorkspaceTrashMapToSectionMigration), ]; match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool) - .run(migrations, ¤t_authenticator) + .run(migrations, &user.authenticator) { Ok(applied_migrations) => { if !applied_migrations.is_empty() { @@ -786,7 +790,13 @@ impl UserManager { ) -> Result<(), FlowyError> { let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?; let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?; - migration_anon_user_on_sign_up(old_user, &old_collab_db, new_user, &new_collab_db)?; + migration_anon_user_on_sign_up( + old_user, + &old_collab_db, + new_user, + &new_collab_db, + authenticator, + )?; match authenticator { Authenticator::Supabase => { diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index 30f2b846b6..244e72f5de 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,11 +1,13 @@ -use crate::services::entities::Session; use flowy_user_deps::entities::UserProfile; +use crate::services::entities::Session; + pub mod document_empty_content; pub mod migration; pub mod session_migration; mod util; pub mod workspace_and_favorite_v1; +pub mod workspace_trash_v1; #[derive(Clone, Debug)] pub struct MigrationUser { diff --git a/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs b/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs index c5fae7bbb4..a3d5f3dfc3 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs @@ -15,23 +15,21 @@ pub fn migrate_session_with_user_uuid( session: &Arc>>, store_preferences: &Arc, ) { - if !store_preferences.get_bool(MIGRATION_USER_NO_USER_UUID) { - if store_preferences + if !store_preferences.get_bool(MIGRATION_USER_NO_USER_UUID) + && store_preferences .set_bool(MIGRATION_USER_NO_USER_UUID, true) .is_ok() - { - if let Some(mut value) = store_preferences.get_object::(&user_config.session_cache_key) - { - if value.get("user_uuid").is_none() { - value.as_object_mut().map(|map| { - map.insert("user_uuid".to_string(), json!(Uuid::new_v4())); - }); + { + if let Some(mut value) = store_preferences.get_object::(&user_config.session_cache_key) { + if value.get("user_uuid").is_none() { + if let Some(map) = value.as_object_mut() { + map.insert("user_uuid".to_string(), json!(Uuid::new_v4())); } + } - if let Ok(new_session) = serde_json::from_value::(value) { - *session.write() = Some(new_session.clone()); - let _ = store_preferences.set_object(&user_config.session_cache_key, &new_session); - } + if let Ok(new_session) = serde_json::from_value::(value) { + *session.write() = Some(new_session.clone()); + let _ = store_preferences.set_object(&user_config.session_cache_key, &new_session); } } } diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs new file mode 100644 index 0000000000..6d6c0e6afc --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use collab_folder::Folder; +use tracing::instrument; + +use collab_integrate::{RocksCollabDB, YrsDocAction}; +use flowy_error::{internal_error, FlowyResult}; +use flowy_user_deps::entities::Authenticator; + +use crate::migrations::migration::UserDataMigration; +use crate::migrations::util::load_collab; +use crate::services::entities::Session; + +/// 1. Migrate the workspace: { trash: [view_id] } to { trash: { uid: [view_id] } } +pub struct WorkspaceTrashMapToSectionMigration; + +impl UserDataMigration for WorkspaceTrashMapToSectionMigration { + fn name(&self) -> &str { + "workspace_trash_map_to_section_migration" + } + + #[instrument(name = "WorkspaceTrashMapToSectionMigration", skip_all, err)] + fn run( + &self, + session: &Session, + collab_db: &Arc, + _authenticator: &Authenticator, + ) -> FlowyResult<()> { + let write_txn = collab_db.write_txn(); + if let Ok(collab) = load_collab(session.user_id, &write_txn, &session.user_workspace.id) { + let folder = Folder::open(session.user_id, collab, None)?; + let trash_ids = folder + .get_trash_v1() + .into_iter() + .map(|fav| fav.id) + .collect::>(); + + if !trash_ids.is_empty() { + folder.add_trash(trash_ids); + } + + let encode = folder.encode_collab_v1(); + write_txn + .flush_doc_with( + session.user_id, + &session.user_workspace.id, + &encode.doc_state, + &encode.state_vector, + ) + .map_err(internal_error)?; + } + + write_txn.commit_transaction().map_err(internal_error)?; + Ok(()) + } +} diff --git a/frontend/rust-lib/flowy-user/src/services/entities.rs b/frontend/rust-lib/flowy-user/src/services/entities.rs index 9035220a4d..c1cb4b447d 100644 --- a/frontend/rust-lib/flowy-user/src/services/entities.rs +++ b/frontend/rust-lib/flowy-user/src/services/entities.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use flowy_user_deps::entities::{AuthResponse, UserProfile, UserWorkspace}; use flowy_user_deps::entities::{Authenticator, UserAuthResponse}; -use crate::entities::AuthTypePB; +use crate::entities::AuthenticatorPB; use crate::migrations::MigrationUser; #[derive(Debug, Clone, Serialize)] @@ -99,7 +99,7 @@ where fn from(value: &T) -> Self { Self { user_id: value.user_id(), - user_uuid: value.user_uuid().clone(), + user_uuid: *value.user_uuid(), user_workspace: value.latest_workspace().clone(), } } @@ -114,22 +114,22 @@ impl std::convert::From for String { } } -impl From for Authenticator { - fn from(pb: AuthTypePB) -> Self { +impl From for Authenticator { + fn from(pb: AuthenticatorPB) -> Self { match pb { - AuthTypePB::Supabase => Authenticator::Supabase, - AuthTypePB::Local => Authenticator::Local, - AuthTypePB::AFCloud => Authenticator::AppFlowyCloud, + AuthenticatorPB::Supabase => Authenticator::Supabase, + AuthenticatorPB::Local => Authenticator::Local, + AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, } } } -impl From for AuthTypePB { +impl From for AuthenticatorPB { fn from(auth_type: Authenticator) -> Self { match auth_type { - Authenticator::Supabase => AuthTypePB::Supabase, - Authenticator::Local => AuthTypePB::Local, - Authenticator::AppFlowyCloud => AuthTypePB::AFCloud, + Authenticator::Supabase => AuthenticatorPB::Supabase, + Authenticator::Local => AuthenticatorPB::Local, + Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, } } } diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index eeeea7431e..e51773e584 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -6,7 +6,7 @@ use tracing::subscriber::set_global_default; use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender}; use tracing_bunyan_formatter::JsonStorageLayer; use tracing_subscriber::fmt::format::Writer; -use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; +use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter}; use crate::layer::FlowyFormattingLayer; @@ -37,22 +37,25 @@ impl Builder { self } - pub fn build(self) -> std::result::Result<(), String> { + pub fn build(self) -> Result<(), String> { let env_filter = EnvFilter::new(self.env_filter); + let std_out_layer = fmt::layer().with_writer(std::io::stdout).pretty(); let (non_blocking, guard) = tracing_appender::non_blocking(self.file_appender); + let file_layer = FlowyFormattingLayer::new(non_blocking); + let subscriber = tracing_subscriber::fmt() .with_timer(CustomTime) .with_ansi(true) .with_target(false) .with_max_level(tracing::Level::TRACE) .with_thread_ids(false) - .with_writer(std::io::stderr) .pretty() .with_env_filter(env_filter) .finish() .with(JsonStorageLayer) - .with(FlowyFormattingLayer::new(non_blocking)); + .with(file_layer) + .with(std_out_layer); set_global_default(subscriber).map_err(|e| format!("{:?}", e))?; diff --git a/frontend/scripts/makefile/tests.toml b/frontend/scripts/makefile/tests.toml index 8f00701530..947b013636 100644 --- a/frontend/scripts/makefile/tests.toml +++ b/frontend/scripts/makefile/tests.toml @@ -1,11 +1,11 @@ # If you want to test a single file with single case, you can try this command: -# RUST_LOG="debug" flutter test -j, --concurrency=1 'path to the file' --name 'test case name' +# RUST_LOG="debug" flutter test -d macOS -j, 'path to the file' --name 'test case name' [tasks.flutter_test] description = "Run flutter test with single case in single file. Input: cargo make flutter_test 'path to the file' --name 'test case name'" script = ''' cd appflowy_flutter -RUST_LOG="debug" flutter test -j, --concurrency=1 "${@}" +flutter test -j, --concurrency=1 "${@}" ''' script_runner = "@shell"