feat: support workspace ops on mobile (#6449)

* feat: support workspace ops on mobile

* chore: move the member bloc to workspace menu item widget

* feat: support creating workspace on mobile

* chore: add popToHome extension

* fix: flutter analyze

* feat: support renaming a workspace

* feat: support deleting a workspace

* feat: support leaving a workspace

* feat: workspace icon ui revamp

* feat: support updating workspace icon on mobile

* feat: show a confirm dialog before deleting a workspace

* fix: workspace name overflow

* feat: support leaving a workspace

* chore: update translations

* feat: show a toast after renaming workspace

* feat: update translations

* feat: add workspace operation integration tests on mobile

* test: add create workspace test on mobile
This commit is contained in:
Lucas 2024-10-02 20:13:19 +08:00 committed by GitHub
parent 23e3650570
commit 813c8e6b86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 976 additions and 327 deletions

View File

@ -39,7 +39,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@ -73,7 +73,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ windows-latest ]
os: [windows-latest]
include:
- os: windows-latest
flutter_profile: development-windows-x86
@ -100,7 +100,7 @@ jobs:
strategy:
fail-fast: true
matrix:
os: [ macos-latest ]
os: [macos-latest]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
@ -122,12 +122,12 @@ jobs:
flutter_profile: ${{ matrix.flutter_profile }}
unit_test:
needs: [ prepare-linux ]
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@ -216,11 +216,11 @@ jobs:
shell: bash
cloud_integration_test:
needs: [ prepare-linux ]
needs: [prepare-linux]
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
flutter_profile: development-linux-x86_64
@ -317,20 +317,20 @@ jobs:
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
docker ps -a
flutter test integration_test/cloud/cloud_runner.dart -d Linux --coverage
flutter test integration_test/desktop/cloud/cloud_runner.dart -d Linux --coverage
shell: bash
# split the integration tests into different machines to minimize the time
integration_test_1:
needs: [ prepare-linux ]
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
@ -352,15 +352,15 @@ jobs:
rust_target: ${{ matrix.target }}
integration_test_2:
needs: [ prepare-linux ]
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
@ -382,15 +382,15 @@ jobs:
rust_target: ${{ matrix.target }}
integration_test_3:
needs: [ prepare-linux ]
needs: [prepare-linux]
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest ]
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
target: "x86_64-unknown-linux-gnu"
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
@ -409,4 +409,4 @@ jobs:
flutter_version: ${{ env.FLUTTER_VERSION }}
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
rust_target: ${{ matrix.target }}

View File

@ -1,93 +0,0 @@
// import 'package:appflowy/env/cloud_env.dart';
// import 'package:appflowy/workspace/application/settings/prelude.dart';
// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:integration_test/integration_test.dart';
// import '../shared/util.dart';
// void main() {
// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// group('supabase auth', () {
// testWidgets('sign in with supabase', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// await tester.expectToSeeHomePageWithGetStartedPage();
// });
// testWidgets('sign out with supabase', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.account);
// await tester.logout();
// // Go to the sign in page again
// await tester.pumpAndSettle(const Duration(seconds: 1));
// tester.expectToSeeGoogleLoginButton();
// });
// testWidgets('sign in as anonymous', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapSignInAsGuest();
// // should not see the sync setting page when sign in as anonymous
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.account);
// // Scroll to sign-out
// await tester.scrollUntilVisible(
// find.byType(SignInOutButton),
// 100,
// scrollable: find.findSettingsScrollable(),
// );
// await tester.tapButton(find.byType(SignInOutButton));
// tester.expectToSeeGoogleLoginButton();
// });
// // testWidgets('enable encryption', (tester) async {
// // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
// // await tester.tapGoogleLoginInButton();
// // // Open the setting page and sign out
// // await tester.openSettings();
// // await tester.openSettingsPage(SettingsPage.cloud);
// // // the switch should be off by default
// // tester.assertEnableEncryptSwitchValue(false);
// // await tester.toggleEnableEncrypt();
// // // the switch should be on after toggling
// // tester.assertEnableEncryptSwitchValue(true);
// // // the switch can not be toggled back to off
// // await tester.toggleEnableEncrypt();
// // tester.assertEnableEncryptSwitchValue(true);
// // });
// testWidgets('enable sync', (tester) async {
// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
// await tester.tapGoogleLoginInButton();
// // Open the setting page and sign out
// await tester.openSettings();
// await tester.openSettingsPage(SettingsPage.cloud);
// // the switch should be on by default
// tester.assertSupabaseEnableSyncSwitchValue(true);
// await tester.toggleEnableSync(SupabaseEnableSync);
// // the switch should be off
// tester.assertSupabaseEnableSyncSwitchValue(false);
// // the switch should be on after toggling
// await tester.toggleEnableSync(SupabaseEnableSync);
// tester.assertSupabaseEnableSyncSwitchValue(true);
// });
// });
// }

View File

@ -1,9 +1,10 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'document/document_drag_block_test.dart' as document_drag_block_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
import 'user_setting_sync_test.dart' as user_sync_test;
import 'uncategorized/anon_user_continue_test.dart' as anon_user_continue_test;
import 'uncategorized/appflowy_cloud_auth_test.dart'
as appflowy_cloud_auth_test;
import 'uncategorized/empty_test.dart' as preset_af_cloud_env_test;
import 'uncategorized/user_setting_sync_test.dart' as user_sync_test;
import 'workspace/change_name_and_icon_test.dart'
as change_workspace_name_and_icon_test;
import 'workspace/collaborative_workspace_test.dart'

View File

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/constants.dart';
import '../../shared/util.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/constants.dart';
import '../../shared/util.dart';
import '../../../shared/constants.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -24,12 +24,12 @@ import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:universal_platform/universal_platform.dart';
import '../../shared/constants.dart';
import '../../shared/database_test_op.dart';
import '../../shared/dir.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../../shared/constants.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -24,10 +24,10 @@ import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -15,8 +15,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -17,9 +17,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -2,7 +2,7 @@ import 'package:appflowy/env/cloud_env.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../shared/util.dart';
import '../../../shared/util.dart';
// This test is meaningless, just for preventing the CI from failing.
void main() {

View File

@ -22,12 +22,12 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/database_test_op.dart';
import '../shared/dir.dart';
import '../shared/emoji.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../board/board_hide_groups_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -15,9 +15,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../shared/workspace.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
import '../../../shared/workspace.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -23,11 +23,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/database_test_op.dart';
import '../../shared/dir.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -27,11 +27,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/database_test_op.dart';
import '../../shared/dir.dart';
import '../../shared/emoji.dart';
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
import '../../../shared/database_test_op.dart';
import '../../../shared/dir.dart';
import '../../../shared/emoji.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -0,0 +1,5 @@
import 'workspace/workspace_operations_test.dart' as workspace_operations_test;
Future<void> main() async {
workspace_operations_test.main();
}

View File

@ -0,0 +1,67 @@
// ignore_for_file: unused_import
import 'dart:io';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.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/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../../shared/constants.dart';
import '../../../shared/dir.dart';
import '../../../shared/mock/mock_file_picker.dart';
import '../../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('workspace operations:', () {
testWidgets('create a new workspace', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
// click the create a new workspace button
await tester.tapButton(find.text(Constants.defaultWorkspaceName));
await tester.tapButton(find.text(LocaleKeys.workspace_create.tr()));
// input the new workspace name
final inputField = find.byType(TextFormField);
const newWorkspaceName = 'AppFlowy';
await tester.enterText(inputField, newWorkspaceName);
await tester.pumpAndSettle();
// wait for the workspace to be created
await tester.pumpUntilFound(
find.text(LocaleKeys.workspace_createSuccess.tr()),
);
// expect to see the new workspace
expect(find.text(newWorkspaceName), findsOneWidget);
});
});
}

View File

@ -1,5 +1,6 @@
import 'package:integration_test/integration_test.dart';
import 'mobile/document/page_style_test.dart' as page_style_test;
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
@ -7,4 +8,5 @@ Future<void> runIntegrationOnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main();
create_new_page_test.main();
page_style_test.main();
}

View File

@ -3,4 +3,6 @@ class Constants {
static const gettingStartedPageName = 'Getting started';
static const toDosPageName = 'To-dos';
static const generalSpaceName = 'General';
static const defaultWorkspaceName = 'My Workspace';
}

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
@ -25,9 +26,15 @@ const String gettingStarted = 'Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
Future<void> expectToSeeHomePageWithGetStartedPage() async {
final finder = find.byType(HomeStack);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
if (UniversalPlatform.isDesktopOrWeb) {
final finder = find.byType(HomeStack);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
} else if (UniversalPlatform.isMobile) {
final finder = find.byType(MobileHomePage);
await pumpUntilFound(finder);
expect(finder, findsOneWidget);
}
final docFinder = find.textContaining(gettingStarted);
await pumpUntilFound(docFinder);

View File

@ -291,7 +291,44 @@ class _HomePageState extends State<_HomePage> {
},
);
break;
case UserWorkspaceActionType.delete:
message = result.fold(
(s) {
toastType = ToastificationType.success;
return LocaleKeys.workspace_deleteSuccess.tr();
},
(e) {
toastType = ToastificationType.error;
return '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}';
},
);
break;
case UserWorkspaceActionType.leave:
message = result.fold(
(s) {
toastType = ToastificationType.success;
return LocaleKeys
.settings_workspacePage_leaveWorkspacePrompt_success
.tr();
},
(e) {
toastType = ToastificationType.error;
return '${LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_fail.tr()}: ${e.msg}';
},
);
break;
case UserWorkspaceActionType.rename:
message = result.fold(
(s) {
toastType = ToastificationType.success;
return LocaleKeys.workspace_renameSuccess.tr();
},
(e) {
toastType = ToastificationType.error;
return '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}';
},
);
break;
default:
message = null;
toastType = ToastificationType.error;

View File

@ -121,30 +121,32 @@ class _MobileWorkspace extends StatelessWidget {
},
child: Row(
children: [
SizedBox.square(
dimension: currentWorkspace.icon.isNotEmpty ? 34.0 : 26.0,
child: WorkspaceIcon(
workspace: currentWorkspace,
iconSize: 26,
fontSize: 16.0,
enableEdit: false,
alignment: Alignment.centerLeft,
figmaLineHeight: 16.0,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
result.emoji,
),
WorkspaceIconV2(
workspace: currentWorkspace,
iconSize: 36,
fontSize: 18.0,
enableEdit: true,
alignment: Alignment.centerLeft,
figmaLineHeight: 26.0,
emojiSize: 24.0,
borderRadius: 12.0,
showBorder: false,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
result.emoji,
),
),
),
),
currentWorkspace.icon.isNotEmpty
? const HSpace(2)
: const HSpace(8),
FlowyText.semibold(
currentWorkspace.name,
fontSize: 20.0,
overflow: TextOverflow.ellipsis,
Flexible(
child: FlowyText.semibold(
currentWorkspace.name,
fontSize: 20.0,
overflow: TextOverflow.ellipsis,
),
),
],
),

View File

@ -0,0 +1,113 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
enum EditWorkspaceNameType {
create,
edit;
String get title {
switch (this) {
case EditWorkspaceNameType.create:
return LocaleKeys.workspace_create.tr();
case EditWorkspaceNameType.edit:
return LocaleKeys.workspace_renameWorkspace.tr();
}
}
String get actionTitle {
switch (this) {
case EditWorkspaceNameType.create:
return LocaleKeys.workspace_create.tr();
case EditWorkspaceNameType.edit:
return LocaleKeys.button_confirm.tr();
}
}
}
class EditWorkspaceNameBottomSheet extends StatefulWidget {
const EditWorkspaceNameBottomSheet({
super.key,
required this.type,
required this.onSubmitted,
required this.workspaceName,
});
final EditWorkspaceNameType type;
final void Function(String) onSubmitted;
// if the workspace name is not empty, it will be used as the initial value of the text field.
final String? workspaceName;
@override
State<EditWorkspaceNameBottomSheet> createState() =>
_EditWorkspaceNameBottomSheetState();
}
class _EditWorkspaceNameBottomSheetState
extends State<EditWorkspaceNameBottomSheet> {
late final TextEditingController _textFieldController;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_textFieldController = TextEditingController(
text: widget.workspaceName,
);
}
@override
void dispose() {
_textFieldController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Form(
key: _formKey,
child: TextFormField(
autofocus: true,
controller: _textFieldController,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: LocaleKeys.workspace_defaultName.tr(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return LocaleKeys.workspace_workspaceNameCannotBeEmpty.tr();
}
return null;
},
onEditingComplete: _onSubmit,
),
),
const VSpace(16),
SizedBox(
width: double.infinity,
child: PrimaryRoundedButton(
text: widget.type.actionTitle,
fontSize: 16,
margin: const EdgeInsets.symmetric(
vertical: 16,
),
onTap: _onSubmit,
),
),
],
);
}
void _onSubmit() {
if (_formKey.currentState!.validate()) {
final value = _textFieldController.text;
widget.onSubmitted.call(value);
}
}
}

View File

@ -1,16 +1,23 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/navigator_context_exntesion.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'create_workspace_menu.dart';
import 'workspace_more_options.dart';
// Only works on mobile.
class MobileWorkspaceMenu extends StatelessWidget {
const MobileWorkspaceMenu({
@ -28,13 +35,13 @@ class MobileWorkspaceMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
// user profile
final List<Widget> children = [
_WorkspaceUserItem(userProfile: userProfile),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Divider(height: 0.5),
),
_buildDivider(),
];
// workspace list
for (var i = 0; i < workspaces.length; i++) {
final workspace = workspaces[i];
children.add(
@ -48,10 +55,97 @@ class MobileWorkspaceMenu extends StatelessWidget {
),
);
}
// create workspace button
children.addAll([
_buildDivider(),
const _CreateWorkspaceButton(),
]);
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
}
Widget _buildDivider() {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Divider(height: 0.5),
);
}
}
class _CreateWorkspaceButton extends StatelessWidget {
const _CreateWorkspaceButton();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FlowyOptionTile.text(
height: 60,
showTopBorder: false,
showBottomBorder: false,
leftIcon: _buildLeftIcon(context),
onTap: () => _showCreateWorkspaceBottomSheet(context),
content: Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: FlowyText.medium(
LocaleKeys.workspace_create.tr(),
fontSize: 14,
),
),
),
),
);
}
void _showCreateWorkspaceBottomSheet(BuildContext context) {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.workspace_create.tr(),
showCloseButton: true,
showDragHandle: true,
showDivider: false,
padding: const EdgeInsets.symmetric(horizontal: 16),
builder: (bottomSheetContext) {
return EditWorkspaceNameBottomSheet(
type: EditWorkspaceNameType.create,
workspaceName: LocaleKeys.workspace_defaultName.tr(),
onSubmitted: (name) {
// create a new workspace
Log.info('create a new workspace: $name');
bottomSheetContext.popToHome();
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.createWorkspace(
name,
),
);
},
);
},
);
}
Widget _buildLeftIcon(BuildContext context) {
return Container(
width: 36.0,
height: 36.0,
padding: const EdgeInsets.all(7.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0x01717171).withOpacity(0.12),
width: 0.8,
),
),
child: const FlowySvg(FlowySvgs.add_workspace_s),
);
}
}
class _WorkspaceUserItem extends StatelessWidget {
@ -107,63 +201,286 @@ class _WorkspaceMenuItem extends StatelessWidget {
)..add(const WorkspaceMemberEvent.initial()),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) {
final members = state.members;
return FlowyOptionTile.text(
content: Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowyText(
workspace.name,
fontSize: 14,
fontWeight: FontWeight.w500,
),
FlowyText(
state.isLoading
? ''
: LocaleKeys.settings_appearance_members_membersCount
.plural(
members.length,
),
fontSize: 10.0,
color: Theme.of(context).hintColor,
),
],
),
),
),
height: 60,
showTopBorder: showTopBorder,
showBottomBorder: false,
leftIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
fontSize: 16.0,
figmaLineHeight: 16.0,
leftIcon: _WorkspaceMenuItemIcon(workspace: workspace),
trailing: _WorkspaceMenuItemTrailing(
workspace: workspace,
currentWorkspace: currentWorkspace,
),
onTap: () => onWorkspaceSelected(workspace),
content: Expanded(
child: _WorkspaceMenuItemContent(
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
onTap: () => onWorkspaceSelected(workspace),
);
},
),
);
}
}
// - Workspace name
// - Workspace member count
class _WorkspaceMenuItemContent extends StatelessWidget {
const _WorkspaceMenuItemContent({
required this.workspace,
});
final UserWorkspacePB workspace;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowyText(
workspace.name,
fontSize: 14,
fontWeight: FontWeight.w500,
overflow: TextOverflow.ellipsis,
),
FlowyText(
context.read<WorkspaceMemberBloc>().state.isLoading
? ''
: LocaleKeys.settings_appearance_members_membersCount.plural(
context.read<WorkspaceMemberBloc>().state.members.length,
),
fontSize: 10.0,
color: Theme.of(context).hintColor,
),
],
),
);
}
}
class _WorkspaceMenuItemIcon extends StatelessWidget {
const _WorkspaceMenuItemIcon({
required this.workspace,
});
final UserWorkspacePB workspace;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: WorkspaceIconV2(
enableEdit: false,
iconSize: 36,
emojiSize: 24.0,
fontSize: 18.0,
figmaLineHeight: 26.0,
borderRadius: 12.0,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
);
}
}
class _WorkspaceMenuItemTrailing extends StatelessWidget {
const _WorkspaceMenuItemTrailing({
required this.workspace,
required this.currentWorkspace,
});
final UserWorkspacePB workspace;
final UserWorkspacePB currentWorkspace;
@override
Widget build(BuildContext context) {
const iconSize = Size.square(20);
return Row(
children: [
const HSpace(12.0),
// show the check icon if the workspace is the current workspace
if (workspace.workspaceId == currentWorkspace.workspaceId)
const FlowySvg(
FlowySvgs.m_blue_check_s,
size: iconSize,
blendMode: null,
),
const HSpace(15.0),
// more options button
AnimatedGestureDetector(
onTapUp: () => _showMoreOptions(context),
child: const FlowySvg(
FlowySvgs.workspace_three_dots_s,
size: iconSize,
blendMode: null,
),
),
const HSpace(8.0),
],
);
}
void _showMoreOptions(BuildContext context) {
final actions =
context.read<WorkspaceMemberBloc>().state.myRole == AFRolePB.Owner
? [
// only the owner can update workspace properties
WorkspaceMenuMoreOption.rename,
WorkspaceMenuMoreOption.delete,
]
: [
WorkspaceMenuMoreOption.leave,
];
showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (bottomSheetContext) {
return WorkspaceMenuMoreOptions(
actions: actions,
onAction: (action) => _onActions(context, bottomSheetContext, action),
);
},
);
}
void _onActions(
BuildContext context,
BuildContext bottomSheetContext,
WorkspaceMenuMoreOption action,
) {
Log.info('execute action in workspace menu bottom sheet: $action');
switch (action) {
case WorkspaceMenuMoreOption.rename:
_showRenameWorkspaceBottomSheet(context);
break;
case WorkspaceMenuMoreOption.invite:
_pushToInviteMembersPage(context);
break;
case WorkspaceMenuMoreOption.delete:
_deleteWorkspace(context, bottomSheetContext);
break;
case WorkspaceMenuMoreOption.leave:
_leaveWorkspace(context, bottomSheetContext);
break;
}
}
void _pushToInviteMembersPage(BuildContext context) {
// empty implementation
// we don't support invite members in workspace menu
}
void _showRenameWorkspaceBottomSheet(BuildContext context) {
showMobileBottomSheet(
context,
showHeader: true,
title: LocaleKeys.workspace_renameWorkspace.tr(),
showCloseButton: true,
showDragHandle: true,
showDivider: false,
padding: const EdgeInsets.symmetric(horizontal: 16),
builder: (bottomSheetContext) {
return EditWorkspaceNameBottomSheet(
type: EditWorkspaceNameType.edit,
workspaceName: workspace.name,
onSubmitted: (name) {
// rename the workspace
Log.info('rename the workspace: $name');
bottomSheetContext.popToHome();
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.renameWorkspace(
workspace.workspaceId,
name,
),
);
},
);
},
);
}
void _deleteWorkspace(BuildContext context, BuildContext bottomSheetContext) {
Navigator.of(bottomSheetContext).pop();
_showConfirmDialog(
context,
'${LocaleKeys.space_delete.tr()}: ${workspace.name}',
LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
LocaleKeys.button_delete.tr(),
(_) async {
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.deleteWorkspace(
workspace.workspaceId,
),
);
context.popToHome();
},
);
}
void _leaveWorkspace(BuildContext context, BuildContext bottomSheetContext) {
Navigator.of(bottomSheetContext).pop();
_showConfirmDialog(
context,
'${LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_title.tr()}: ${workspace.name}',
LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_content.tr(),
LocaleKeys.button_confirm.tr(),
(_) async {
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.leaveWorkspace(
workspace.workspaceId,
),
);
context.popToHome();
},
);
}
void _showConfirmDialog(
BuildContext context,
String title,
String content,
String rightButtonText,
void Function(BuildContext context)? onRightButtonPressed,
) {
showFlowyCupertinoConfirmDialog(
title: title,
content: FlowyText(
content,
fontSize: 14,
color: Theme.of(context).hintColor,
maxLines: 10,
),
leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w500,
color: const Color(0xFF007AFF),
),
rightButton: FlowyText(
rightButtonText,
fontSize: 17.0,
figmaLineHeight: 24.0,
fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: onRightButtonPressed,
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
enum WorkspaceMenuMoreOption {
rename,
invite,
delete,
leave,
}
class WorkspaceMenuMoreOptions extends StatelessWidget {
const WorkspaceMenuMoreOptions({
super.key,
this.isFavorite = false,
required this.onAction,
required this.actions,
});
final bool isFavorite;
final void Function(WorkspaceMenuMoreOption action) onAction;
final List<WorkspaceMenuMoreOption> actions;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: actions
.map(
(action) => _buildActionButton(context, action),
)
.toList(),
);
}
Widget _buildActionButton(
BuildContext context,
WorkspaceMenuMoreOption action,
) {
switch (action) {
case WorkspaceMenuMoreOption.rename:
return FlowyOptionTile.text(
text: LocaleKeys.button_rename.tr(),
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.view_item_rename_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
WorkspaceMenuMoreOption.rename,
),
);
case WorkspaceMenuMoreOption.delete:
return FlowyOptionTile.text(
text: LocaleKeys.button_delete.tr(),
height: 52.0,
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.trash_s,
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
WorkspaceMenuMoreOption.delete,
),
);
case WorkspaceMenuMoreOption.invite:
return FlowyOptionTile.text(
// i18n
text: 'Invite',
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.workspace_add_member_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
WorkspaceMenuMoreOption.invite,
),
);
case WorkspaceMenuMoreOption.leave:
return FlowyOptionTile.text(
text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),
height: 52.0,
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.leave_workspace_s,
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
WorkspaceMenuMoreOption.leave,
),
);
default:
return const Placeholder();
}
}
}

View File

@ -91,6 +91,7 @@ Future<T?> showFlowyMobileConfirmDialog<T>(
Future<T?> showFlowyCupertinoConfirmDialog<T>({
BuildContext? context,
required String title,
Widget? content,
required Widget leftButton,
required Widget rightButton,
void Function(BuildContext context)? onLeftButtonPressed,
@ -106,6 +107,7 @@ Future<T?> showFlowyCupertinoConfirmDialog<T>({
maxLines: 10,
figmaLineHeight: 22.0,
),
content: content,
actions: [
CupertinoDialogAction(
onPressed: () {

View File

@ -158,6 +158,7 @@ class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> {
const VSpace(padding),
],
MobileThirdPartySignInButton(
key: signInWithGoogleButtonKey,
type: ThirdPartySignInButtonType.google,
onPressed: () => widget.onSignIn(ThirdPartySignInButtonType.google),
),

View File

@ -1,9 +1,28 @@
import 'package:flutter/material.dart';
// the color set generated from AI
final _builtInColorSet = [
(const Color(0xFF8A2BE2), const Color(0xFFF0E6FF)),
(const Color(0xFF2E8B57), const Color(0xFFE0FFF0)),
(const Color(0xFF1E90FF), const Color(0xFFE6F3FF)),
(const Color(0xFFFF7F50), const Color(0xFFFFF0E6)),
(const Color(0xFFFF69B4), const Color(0xFFFFE6F0)),
(const Color(0xFF20B2AA), const Color(0xFFE0FFFF)),
(const Color(0xFFDC143C), const Color(0xFFFFE6E6)),
(const Color(0xFF8B4513), const Color(0xFFFFF0E6)),
];
extension type ColorGenerator(String value) {
Color toColor() {
final int hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit);
final double hue = (hash % 360).toDouble();
return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor();
}
// shuffle a color from the built-in color set, for the same name, the result should be the same
(Color, Color) randomColor() {
final hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit);
final index = hash % _builtInColorSet.length;
return _builtInColorSet[index];
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
extension NavigatorContext on BuildContext {
void popToHome() {
Navigator.of(this).popUntil((route) {
if (route.settings.name == '/') {
return true;
}
return false;
});
}
}

View File

@ -1,11 +1,16 @@
import 'dart:math';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_platform/universal_platform.dart';
class WorkspaceIcon extends StatefulWidget {
const WorkspaceIcon({
@ -95,3 +100,120 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
return child;
}
}
// The v2 supports the built-in color set
class WorkspaceIconV2 extends StatefulWidget {
const WorkspaceIconV2({
super.key,
required this.workspace,
required this.enableEdit,
required this.iconSize,
required this.fontSize,
required this.onSelected,
this.borderRadius = 4,
this.emojiSize,
this.alignment,
required this.figmaLineHeight,
this.showBorder = true,
});
final UserWorkspacePB workspace;
final double iconSize;
final bool enableEdit;
final double fontSize;
final double? emojiSize;
final void Function(EmojiPickerResult) onSelected;
final double borderRadius;
final Alignment? alignment;
final double figmaLineHeight;
final bool showBorder;
@override
State<WorkspaceIconV2> createState() => _WorkspaceIconV2State();
}
class _WorkspaceIconV2State extends State<WorkspaceIconV2> {
final controller = PopoverController();
@override
Widget build(BuildContext context) {
final color = ColorGenerator(widget.workspace.name).randomColor();
Widget child = widget.workspace.icon.isNotEmpty
? FlowyText.emoji(
widget.workspace.icon,
fontSize: widget.emojiSize,
figmaLineHeight: widget.figmaLineHeight,
optimizeEmojiAlign: true,
)
: FlowyText.semibold(
widget.workspace.name.isEmpty
? ''
: widget.workspace.name.substring(0, 1),
fontSize: widget.fontSize,
color: color.$1,
);
child = Container(
alignment: Alignment.center,
width: widget.iconSize,
height: widget.iconSize,
decoration: BoxDecoration(
color: widget.workspace.icon.isNotEmpty ? null : color.$2,
borderRadius: BorderRadius.circular(widget.borderRadius),
border: widget.showBorder
? Border.all(
color: const Color(0x1A717171),
)
: null,
),
child: child,
);
if (widget.enableEdit) {
child = _buildEditableIcon(child);
}
return child;
}
Widget _buildEditableIcon(Widget child) {
if (UniversalPlatform.isDesktopOrWeb) {
AppFlowyPopover(
offset: const Offset(0, 8),
controller: controller,
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(364, 356)),
clickHandler: PopoverClickHandler.gestureDetector,
margin: const EdgeInsets.all(0),
popupBuilder: (_) => FlowyIconEmojiPicker(
onSelectedEmoji: (result) {
widget.onSelected(result);
controller.close();
},
),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
);
}
return GestureDetector(
onTap: () async {
final result = await context.push<EmojiPickerResult>(
Uri(
path: MobileEmojiPickerScreen.routeName,
queryParameters: {
MobileEmojiPickerScreen.pageTitle:
LocaleKeys.settings_workspacePage_workspaceIcon_title.tr(),
},
).toString(),
);
if (result != null) {
widget.onSelected(result);
}
},
child: child,
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/util/navigator_context_exntesion.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
@ -100,12 +101,7 @@ class _AccountDeletionButtonState extends State<AccountDeletionButton> {
textEditingController.text.trim(),
isCheckedNotifier.value,
onSuccess: () {
Navigator.of(context).popUntil((route) {
if (route.settings.name == '/') {
return true;
}
return false;
});
context.popToHome();
},
),
);

View File

@ -5,6 +5,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/prelude.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart';
import 'package:appflowy/util/navigator_context_exntesion.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@ -89,13 +90,7 @@ class _SignInDialogContent extends StatelessWidget {
const VSpace(10),
SettingThirdPartyLogin(
didLogin: () {
// dismiss the setting dialog
Navigator.of(context).popUntil((route) {
if (route.settings.name == '/') {
return true;
}
return false;
});
context.popToHome();
},
),
],

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/navigator_context_exntesion.dart';
import 'package:appflowy/workspace/application/export/document_exporter.dart';
import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';
import 'package:appflowy/workspace/application/settings/share/export_service.dart';
@ -132,9 +133,7 @@ class _FileExporterWidgetState extends State<FileExporterWidget> {
);
}
if (mounted) {
Navigator.of(context).popUntil(
(router) => router.settings.name == '/',
);
context.popToHome();
}
});
},

View File

@ -862,14 +862,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
functions_client:
dependency: transitive
description:
name: functions_client
sha256: e63f49cd3b41727f47b3bde284a11a4ac62839e0604f64077d4257487510e484
url: "https://pub.dev"
source: hosted
version: "2.3.2"
get_it:
dependency: "direct main"
description:
@ -902,14 +894,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.2.1"
gotrue:
dependency: transitive
description:
name: gotrue
sha256: "8703db795511f69194fe77125a0c838bbb6befc2f95717b6e40331784a8bdecb"
url: "https://pub.dev"
source: hosted
version: "2.8.4"
graphs:
dependency: transitive
description:
@ -1139,14 +1123,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.8.0"
jwt_decode:
dependency: transitive
description:
name: jwt_decode
sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb
url: "https://pub.dev"
source: hosted
version: "0.3.1"
keyboard_height_plugin:
dependency: "direct main"
description:
@ -1579,14 +1555,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
postgrest:
dependency: transitive
description:
name: postgrest
sha256: c4197238601c7c3103b03a4bb77f2050b17d0064bf8b968309421abdebbb7f0e
url: "https://pub.dev"
source: hosted
version: "2.1.4"
process:
dependency: transitive
description:
@ -1635,14 +1603,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: d897a65ee3b1b5ddc1cf606f0b83792262d38fd5679c2df7e38da29c977513da
url: "https://pub.dev"
source: hosted
version: "2.2.1"
recase:
dependency: transitive
description:
@ -1667,14 +1627,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
retry:
dependency: transitive
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
run_with_network_images:
dependency: "direct dev"
description:
@ -1961,14 +1913,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
storage_client:
dependency: transitive
description:
name: storage_client
sha256: "28c147c805304dbc2b762becd1fc26ee0cb621ace3732b9ae61ef979aab8b367"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
stream_channel:
dependency: transitive
description:
@ -2009,23 +1953,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.1"
supabase:
dependency: transitive
description:
name: supabase
sha256: "4ed1cf3298f39865c05b2d8557f92eb131a9b9af70e32e218672a0afce01a6bc"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
supabase_flutter:
dependency: "direct main"
description:
path: "packages/supabase_flutter"
ref: "9b05eea"
resolved-ref: "9b05eeac559a1f2da6289e1d70b3fa89e262fa3c"
url: "https://github.com/supabase/supabase-flutter"
source: git
version: "2.3.1"
super_clipboard:
dependency: "direct main"
description:
@ -2403,14 +2330,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
yet_another_json_isolate:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: "47ed3900e6b0e4dfe378811a4402e85b7fc126a7daa94f840fef65ea9c8e46f4"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"

View File

@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.29688 5.66993C6.52938 2.96993 7.91688 1.86743 10.9544 1.86743H11.0519C14.4044 1.86743 15.7469 3.20993 15.7469 6.56243V11.4524C15.7469 14.8049 14.4044 16.1474 11.0519 16.1474H10.9544C7.93938 16.1474 6.55188 15.0599 6.30438 12.4049" stroke="#FB006D" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 9H11.16" stroke="#FB006D" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.48438 6.4873L11.9969 8.9998L9.48438 11.5123" stroke="#FB006D" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25C5.25 7.32107 6.92893 9 9 9Z" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.55859 16.5C2.55859 13.5975 5.44609 11.25 9.00109 11.25C9.72109 11.25 10.4186 11.3475 11.0711 11.5275" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 13.5C16.5 13.74 16.47 13.9725 16.41 14.1975C16.3425 14.4975 16.2225 14.79 16.065 15.045C15.5475 15.915 14.595 16.5 13.5 16.5C12.7275 16.5 12.03 16.2075 11.505 15.7275C11.28 15.5325 11.085 15.3 10.935 15.045C10.6575 14.595 10.5 14.0625 10.5 13.5C10.5 12.69 10.8225 11.9475 11.3475 11.4075C11.895 10.845 12.66 10.5 13.5 10.5C14.385 10.5 15.1875 10.8825 15.7275 11.4975C16.2075 12.03 16.5 12.735 16.5 13.5Z" stroke="#171717" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.6178 13.4849H12.3828" stroke="#171717" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 12.3899V14.6324" stroke="#171717" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -72,9 +72,11 @@
},
"workspace": {
"chooseWorkspace": "Choose your workspace",
"defaultName": "My Workspace",
"create": "Create workspace",
"reset": "Reset workspace",
"renameWorkspace": "Rename workspace",
"workspaceNameCannotBeEmpty": "Workspace name cannot be empty",
"resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace",
"hint": "workspace",
"notFoundError": "Workspace not found",
@ -509,7 +511,9 @@
},
"leaveWorkspacePrompt": {
"title": "Leave workspace",
"content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it."
"content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it.",
"success": "You have left the workspace successfully.",
"fail": "Failed to leave the workspace."
},
"manageWorkspace": {
"title": "Manage workspace",
@ -2661,4 +2665,4 @@
"refreshNote": "After successful upgrade, click <refresh/> to activate your new features.",
"refresh": "here"
}
}
}