From c51b495544e52d2590b9de4684914bac518f9505 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 23 Sep 2024 20:19:29 +0800 Subject: [PATCH] feat: support appflowy cloud in sign-in settings page (#6386) * feat: support appflowy cloud in sign-in settings page * test: add cloud server test * test: wait until the tooltip disappear --- .../desktop/settings/settings_runner.dart | 2 + .../settings/sign_in_page_settings_test.dart | 103 +++++++++ .../lib/flutter/af_dropdown_menu.dart | 26 ++- .../desktop_sign_in_screen.dart | 8 +- .../settings/settings_dialog.dart | 204 +++++++++++++----- .../settings/shared/settings_dropdown.dart | 3 +- 6 files changed, 281 insertions(+), 65 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart index 34fe511e9e..617d495265 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart @@ -3,6 +3,7 @@ import 'package:integration_test/integration_test.dart'; import 'notifications_settings_test.dart' as notifications_settings_test; import 'settings_billing_test.dart' as settings_billing_test; import 'shortcuts_settings_test.dart' as shortcuts_settings_test; +import 'sign_in_page_settings_test.dart' as sign_in_page_settings_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -10,4 +11,5 @@ void main() { notifications_settings_test.main(); settings_billing_test.main(); shortcuts_settings_test.main(); + sign_in_page_settings_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart new file mode 100644 index 0000000000..7caf439b66 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; +import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:toastification/toastification.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + Finder findServerType(AuthenticatorType type) { + return find + .descendant( + of: find.byType(SettingsServerDropdownMenu), + matching: find.findTextInFlowyText( + type.label, + ), + ) + .last; + } + + group('sign-in page settings: ', () { + testWidgets('change server type', (tester) async { + await tester.initializeAppFlowy(); + + // reset the app to the default state + await useAppFlowyBetaCloudWithURL( + kAppflowyCloudUrl, + AuthenticatorType.appflowyCloud, + ); + + // open the settings page + final settingsButton = find.byType(DesktopSignInSettingsButton); + await tester.tapButton(settingsButton); + + expect(find.byType(SimpleSettingsDialog), findsOneWidget); + + // the default type should be appflowy cloud + final appflowyCloudType = findServerType(AuthenticatorType.appflowyCloud); + expect(appflowyCloudType, findsOneWidget); + + // change the server type to self-host + await tester.tapButton(appflowyCloudType); + final selfhostedButton = findServerType( + AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapButton(selfhostedButton); + + // update server url + const serverUrl = 'https://test.appflowy.cloud'; + await tester.enterText( + find.byKey(kSelfHostedTextInputFieldKey), + serverUrl, + ); + await tester.pumpAndSettle(); + await tester.tapButton( + find.findTextInFlowyText(LocaleKeys.button_save.tr()), + ); + + // wait the app to restart, and the tooltip to disappear + await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder)); + await tester.pumpAndSettle(const Duration(milliseconds: 250)); + + // open settings page to check the result + await tester.tapButton(settingsButton); + + // check the server type + expect( + findServerType(AuthenticatorType.appflowyCloudSelfHost), + findsOneWidget, + ); + // check the server url + expect( + find.text(serverUrl), + findsOneWidget, + ); + + // reset to appflowy cloud + await tester.tapButton( + findServerType(AuthenticatorType.appflowyCloudSelfHost), + ); + // change the server type to appflowy cloud + await tester.tapButton( + findServerType(AuthenticatorType.appflowyCloud), + ); + + // wait the app to restart, and the tooltip to disappear + await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder)); + await tester.pumpAndSettle(const Duration(milliseconds: 250)); + + // check the server type + await tester.tapButton(settingsButton); + expect( + findServerType(AuthenticatorType.appflowyCloud), + findsOneWidget, + ); + }); + }); +} diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index 9d18bb14f7..1cc846339b 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -391,17 +391,23 @@ class _AFDropdownMenuState extends State> { ); } + // Remove the code here, it will throw a FlutterError + // Unless we upgrade to Flutter 3.24 https://github.com/flutter/flutter/issues/146764 void scrollToHighlight() { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - final BuildContext? highlightContext = - buttonItemKeys[currentHighlight!].currentContext; - if (highlightContext != null) { - Scrollable.ensureVisible(highlightContext); - } - }, - debugLabel: 'DropdownMenu.scrollToHighlight', - ); + // WidgetsBinding.instance.addPostFrameCallback( + // (_) { + // // try { + // final BuildContext? highlightContext = + // buttonItemKeys[currentHighlight!].currentContext; + // if (highlightContext != null) { + // Scrollable.ensureVisible(highlightContext); + // } + // } catch (_) { + // return; + // } + // }, + // debugLabel: 'DropdownMenu.scrollToHighlight', + // ); } double? getWidth(GlobalKey key) { diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 6a7509be83..94b4347869 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -68,7 +68,7 @@ class DesktopSignInScreen extends StatelessWidget { const Row( mainAxisSize: MainAxisSize.min, children: [ - _SettingsButton(), + DesktopSignInSettingsButton(), HSpace(42), SignInAnonymousButtonV2(), ], @@ -92,8 +92,10 @@ class DesktopSignInScreen extends StatelessWidget { } } -class _SettingsButton extends StatelessWidget { - const _SettingsButton(); +class DesktopSignInSettingsButton extends StatelessWidget { + const DesktopSignInSettingsButton({ + super.key, + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 99f20809b4..b59afa3e22 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -14,7 +14,9 @@ import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_d import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; @@ -30,6 +32,10 @@ import 'package:toastification/toastification.dart'; import 'widgets/setting_cloud.dart'; +@visibleForTesting +const kSelfHostedTextInputFieldKey = + ValueKey('self_hosted_url_input_text_field'); + class SettingsDialog extends StatelessWidget { SettingsDialog( this.user, { @@ -173,31 +179,33 @@ class _SimpleSettingsDialogState extends State { return FlowyDialog( width: MediaQuery.of(context).size.width * 0.7, constraints: const BoxConstraints(maxWidth: 784, minWidth: 564), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // header - FlowyText( - LocaleKeys.signIn_settings.tr(), - fontSize: 36.0, - fontWeight: FontWeight.w600, - ), - const VSpace(18.0), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // header + FlowyText( + LocaleKeys.signIn_settings.tr(), + fontSize: 36.0, + fontWeight: FontWeight.w600, + ), + const VSpace(18.0), - // language - _LanguageSettings(key: ValueKey('language${settings.hashCode}')), - const VSpace(22.0), + // language + _LanguageSettings(key: ValueKey('language${settings.hashCode}')), + const VSpace(22.0), - // self-host cloud - _SelfHostSettings(key: ValueKey('selfhost${settings.hashCode}')), - const VSpace(22.0), + // self-host cloud + _SelfHostSettings(key: ValueKey('selfhost${settings.hashCode}')), + const VSpace(22.0), - // support - _SupportSettings(key: ValueKey('support${settings.hashCode}')), - ], + // support + _SupportSettings(key: ValueKey('support${settings.hashCode}')), + ], + ), ), ), ); @@ -229,6 +237,7 @@ class _SelfHostSettings extends StatefulWidget { class _SelfHostSettingsState extends State<_SelfHostSettings> { final textController = TextEditingController(); + AuthenticatorType type = AuthenticatorType.appflowyCloud; @override void initState() { @@ -236,6 +245,11 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { getAppFlowyCloudUrl().then((url) { textController.text = url; + if (kAppflowyCloudUrl != url) { + setState(() { + type = AuthenticatorType.appflowyCloudSelfHost; + }); + } }); } @@ -248,42 +262,81 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { @override Widget build(BuildContext context) { return SettingsCategory( - title: LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(), + title: LocaleKeys.settings_menu_cloudAppFlowy.tr(), children: [ - Row( - children: [ - Expanded( - child: SizedBox( - height: 36, - child: FlowyTextField( - controller: textController, - autoFocus: false, - textStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - hintText: 'https://beta.appflowy.cloud', - onEditingComplete: _saveSelfHostUrl, - ), + Flexible( + child: SettingsServerDropdownMenu( + selectedServer: type, + onSelected: _onSelected, + ), + ), + if (type == AuthenticatorType.appflowyCloudSelfHost) _buildInputField(), + ], + ); + } + + Widget _buildInputField() { + return Row( + children: [ + Expanded( + child: SizedBox( + height: 36, + child: FlowyTextField( + key: kSelfHostedTextInputFieldKey, + controller: textController, + autoFocus: false, + textStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + ), + hintText: kAppflowyCloudUrl, + onEditingComplete: () => _saveUrl( + url: textController.text, + type: AuthenticatorType.appflowyCloudSelfHost, ), ), - const HSpace(12.0), - Container( - height: 36, - constraints: const BoxConstraints(minWidth: 78), - child: OutlinedRoundedButton( - text: LocaleKeys.button_save.tr(), - onTap: _saveSelfHostUrl, - ), + ), + ), + const HSpace(12.0), + Container( + height: 36, + constraints: const BoxConstraints(minWidth: 78), + child: OutlinedRoundedButton( + text: LocaleKeys.button_save.tr(), + onTap: () => _saveUrl( + url: textController.text, + type: AuthenticatorType.appflowyCloudSelfHost, ), - ], + ), ), ], ); } - void _saveSelfHostUrl() { - final url = textController.text; + void _onSelected(AuthenticatorType type) { + if (type == this.type) { + return; + } + + Log.info('Switching server type to $type'); + + setState(() { + this.type = type; + }); + + if (type == AuthenticatorType.appflowyCloud) { + textController.text = kAppflowyCloudUrl; + _saveUrl( + url: textController.text, + type: type, + ); + } + } + + void _saveUrl({ + required String url, + required AuthenticatorType type, + }) { if (url.isEmpty) { showToastNotification( context, @@ -301,7 +354,7 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { ); Navigator.of(context).pop(); - await useSelfHostedAppFlowyCloudWithURL(url); + await useAppFlowyBetaCloudWithURL(url, type); await runAppFlowy(); }, (err) { @@ -316,6 +369,57 @@ class _SelfHostSettingsState extends State<_SelfHostSettings> { } } +@visibleForTesting +extension SettingsServerDropdownMenuExtension on AuthenticatorType { + String get label { + switch (this) { + case AuthenticatorType.appflowyCloud: + return LocaleKeys.settings_menu_cloudAppFlowy.tr(); + case AuthenticatorType.appflowyCloudSelfHost: + return LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(); + default: + throw Exception('Unsupported server type: $this'); + } + } +} + +@visibleForTesting +class SettingsServerDropdownMenu extends StatelessWidget { + const SettingsServerDropdownMenu({ + super.key, + required this.selectedServer, + required this.onSelected, + }); + + final AuthenticatorType selectedServer; + final void Function(AuthenticatorType type) onSelected; + + // in the settings page from sign in page, we only support appflowy cloud and self-hosted + static final supportedServers = [ + AuthenticatorType.appflowyCloud, + AuthenticatorType.appflowyCloudSelfHost, + ]; + + @override + Widget build(BuildContext context) { + return SettingsDropdown( + expandWidth: false, + onChanged: onSelected, + selectedOption: selectedServer, + options: supportedServers + .map( + (serverType) => buildDropdownMenuEntry( + context, + selectedValue: selectedServer, + value: serverType, + label: serverType.label, + ), + ) + .toList(), + ); + } +} + class _SupportSettings extends StatelessWidget { const _SupportSettings({ super.key, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 91d8e1b3e7..18cacd6c4b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/flutter/af_dropdown_menu.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -7,6 +5,7 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan import 'package:collection/collection.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsDropdown extends StatefulWidget {