feat: open workspace, rename workspace and update workspace icon (#4818)

* feat: support opening a workspace

* feat: support renaming a workspace

* fix: rename issue

* fix: rename issues

* feat: refactor menu bloc

* test: add collaborative workspace test

* test: add open workspace integration tet

* test: add delete workspace integration tet

* chore: turn off collab workspace feature
This commit is contained in:
Lucas.Xu 2024-03-05 13:51:03 +08:00 committed by GitHub
parent c0210a5778
commit c8e86f4f26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 946 additions and 367 deletions

View File

@ -1,8 +1,9 @@
import 'empty_test.dart' as preset_af_cloud_env_test;
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
import 'collaborative_workspace_test.dart' as collaboration_workspace_test;
import 'empty_test.dart' as preset_af_cloud_env_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<void> main() async {
preset_af_cloud_env_test.main();
@ -14,4 +15,6 @@ Future<void> main() async {
user_sync_test.main();
anon_user_continue_test.main();
collaboration_workspace_test.main();
}

View File

@ -0,0 +1,115 @@
// 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/shared/feature_flags.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/home/menu/sidebar/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.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/workspace/presentation/widgets/user_avatar.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 '../util/database_test_op.dart';
import '../util/dir.dart';
import '../util/emoji.dart';
import '../util/mock/mock_file_picker.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () {
// only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) {
return;
}
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
const name = 'AppFlowy.IO';
await tester.createCollaborativeWorkspace(name);
// see the success message
var success = find.text(LocaleKeys.workspace_createSuccess.tr());
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
// check the create result
await tester.openCollaborativeWorkspaceMenu();
var items = find.byType(WorkspaceMenuItem);
expect(items, findsNWidgets(2));
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
name,
);
// open the newly created workspace
await tester.tapButton(items.last);
success = find.text(LocaleKeys.workspace_openSuccess.tr());
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
await tester.closeCollaborativeWorkspaceMenu();
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
final secondWorkspace = find.byType(WorkspaceMenuItem).last;
await tester.hoverOnWidget(
secondWorkspace,
onHover: () async {
// click the more button
final moreButton = find.byType(WorkspaceMoreActionList);
expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton);
// click the delete button
final deleteButton = find.text(LocaleKeys.button_delete.tr());
expect(deleteButton, findsOneWidget);
await tester.tapButton(deleteButton);
// see the delete confirm dialog
final confirm =
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
expect(confirm, findsOneWidget);
await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// delete success
success = find.text(LocaleKeys.workspace_createSuccess.tr());
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
},
);
// check the result
await tester.openCollaborativeWorkspaceMenu();
items = find.byType(WorkspaceMenuItem);
expect(items, findsOneWidget);
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name != name,
true,
);
await tester.closeCollaborativeWorkspaceMenu();
});
});
}

View File

@ -134,8 +134,9 @@ extension AppFlowyTestBase on WidgetTester {
Future<void> pumpUntilFound(
Finder finder, {
Duration timeout = const Duration(seconds: 10),
Duration pumpInterval =
const Duration(milliseconds: 50), // Interval between pumps
Duration pumpInterval = const Duration(
milliseconds: 50,
), // Interval between pumps
}) async {
bool timerDone = false;
final timer = Timer(timeout, () => timerDone = true);
@ -148,6 +149,24 @@ extension AppFlowyTestBase on WidgetTester {
timer.cancel();
}
Future<void> pumpUntilNotFound(
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) {
await pump(pumpInterval); // Pump with an interval
if (!any(finder)) {
break;
}
}
timer.cancel();
}
Future<void> tapButton(
Finder finder, {
int? pointer,

View File

@ -6,10 +6,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
@ -518,6 +521,51 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
}
Future<void> openCollaborativeWorkspaceMenu() async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
}
final workspace = find.byType(SidebarWorkspace);
expect(workspace, findsOneWidget);
// click it
await tapButton(workspace);
}
Future<void> closeCollaborativeWorkspaceMenu() async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
}
await tapAt(Offset.zero);
await pumpAndSettle();
}
Future<void> createCollaborativeWorkspace(String name) async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
}
await openCollaborativeWorkspaceMenu();
// expect to see the workspace list, and there should be only one workspace
final workspacesMenu = find.byType(WorkspacesMenu);
expect(workspacesMenu, findsOneWidget);
// click the create button
final createButton = find.byKey(createWorkspaceButtonKey);
expect(createButton, findsOneWidget);
await tapButton(createButton);
// see the create workspace dialog
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
expect(createWorkspaceDialog, findsOneWidget);
// input the workspace name
await enterText(find.byType(TextField), name);
await pumpAndSettle();
await tapButtonWithName(LocaleKeys.button_ok.tr());
}
}
extension ViewLayoutPBTest on ViewLayoutPB {

View File

@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
class NotificationParser<T, E> {
class NotificationParser<T, E extends Object> {
NotificationParser({
this.id,
required this.callback,

View File

@ -3,7 +3,7 @@ import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -27,10 +27,13 @@ class MobileFavoritePageFolder extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => MenuBloc(
user: userProfile,
workspaceId: workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
create: (_) => SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
),
),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
@ -38,11 +41,11 @@ class MobileFavoritePageFolder extends StatelessWidget {
],
child: MultiBlocListener(
listeners: [
BlocListener<MenuBloc, MenuState>(
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
listenWhen: (p, c) =>
p.lastCreatedView?.id != c.lastCreatedView?.id,
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedView!),
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(

View File

@ -1,7 +1,7 @@
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -26,10 +26,13 @@ class MobileFolders extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => MenuBloc(
user: user,
workspaceId: workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
create: (_) => SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
user,
workspaceSetting.workspaceId,
),
),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
@ -37,16 +40,16 @@ class MobileFolders extends StatelessWidget {
],
child: MultiBlocListener(
listeners: [
BlocListener<MenuBloc, MenuState>(
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
listenWhen: (p, c) =>
p.lastCreatedView?.id != c.lastCreatedView?.id,
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedView!),
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final menuState = context.watch<MenuBloc>().state;
final menuState = context.watch<SidebarRootViewsBloc>().state;
return SlidableAutoCloseBehavior(
child: Column(
children: [

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -67,8 +67,8 @@ class _MobilePersonalFolderHeaderState
size: Size.square(iconSize),
),
onPressed: () {
context.read<MenuBloc>().add(
MenuEvent.createApp(
context.read<SidebarRootViewsBloc>().add(
SidebarRootViewsEvent.createRootView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
),

View File

@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
@ -80,11 +80,14 @@ class _NotificationScreenContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => MenuBloc(
workspaceId: workspaceSetting.workspaceId,
user: userProfile,
)..add(const MenuEvent.initial()),
child: BlocBuilder<MenuBloc, MenuState>(
create: (_) => SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
),
),
child: BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
builder: (context, menuState) =>
BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
builder: (context, filterState) =>

View File

@ -130,4 +130,24 @@ class UserBackendService {
final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
return UserEventDeleteWorkspace(request).send();
}
Future<FlowyResult<void, FlowyError>> renameWorkspace(
String workspaceId,
String name,
) {
final request = RenameWorkspacePB()
..workspaceId = workspaceId
..newName = name;
return UserEventRenameWorkspace(request).send();
}
Future<FlowyResult<void, FlowyError>> updateWorkspaceIcon(
String workspaceId,
String icon,
) {
final request = ChangeWorkspaceIconPB()
..workspaceId = workspaceId
..newIcon = icon;
return UserEventChangeWorkspaceIcon(request).send();
}
}

View File

@ -1,139 +0,0 @@
import 'dart:async';
import 'package:appflowy/workspace/application/workspace/workspace_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'menu_bloc.freezed.dart';
class MenuBloc extends Bloc<MenuEvent, MenuState> {
MenuBloc({required this.user, required this.workspaceId})
: _workspaceService = WorkspaceService(workspaceId: workspaceId),
_listener = WorkspaceListener(
user: user,
workspaceId: workspaceId,
),
super(MenuState.initial()) {
_dispatch();
}
final WorkspaceService _workspaceService;
final WorkspaceListener _listener;
final UserProfilePB user;
final String workspaceId;
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
void _dispatch() {
on<MenuEvent>(
(event, emit) async {
await event.map(
initial: (e) async {
_listener.start(appsChanged: _handleAppsOrFail);
await _fetchApps(emit);
},
createApp: (_CreateApp event) async {
final result = await _workspaceService.createApp(
name: event.name,
desc: event.desc,
index: event.index,
);
result.fold(
(app) => emit(state.copyWith(lastCreatedView: app)),
(error) {
Log.error(error);
emit(
state.copyWith(
successOrFailure: FlowyResult.failure(error),
),
);
},
);
},
didReceiveApps: (e) async {
emit(
e.appsOrFail.fold(
(views) => state.copyWith(
views: views,
successOrFailure: FlowyResult.success(null),
),
(err) =>
state.copyWith(successOrFailure: FlowyResult.failure(err)),
),
);
},
moveApp: (_MoveApp value) {
if (state.views.length > value.fromIndex) {
final view = state.views[value.fromIndex];
_workspaceService.moveApp(
appId: view.id,
fromIndex: value.fromIndex,
toIndex: value.toIndex,
);
final apps = List<ViewPB>.from(state.views);
apps.insert(value.toIndex, apps.removeAt(value.fromIndex));
emit(state.copyWith(views: apps));
}
},
);
},
);
}
// ignore: unused_element
Future<void> _fetchApps(Emitter<MenuState> emit) async {
final viewsOrError = await _workspaceService.getViews();
emit(
viewsOrError.fold(
(views) => state.copyWith(views: views),
(error) {
Log.error(error);
return state.copyWith(successOrFailure: FlowyResult.failure(error));
},
),
);
}
void _handleAppsOrFail(FlowyResult<List<ViewPB>, FlowyError> appsOrFail) {
appsOrFail.fold(
(apps) => add(MenuEvent.didReceiveApps(FlowyResult.success(apps))),
(error) => add(MenuEvent.didReceiveApps(FlowyResult.failure(error))),
);
}
}
@freezed
class MenuEvent with _$MenuEvent {
const factory MenuEvent.initial() = _Initial;
const factory MenuEvent.createApp(String name, {String? desc, int? index}) =
_CreateApp;
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
const factory MenuEvent.didReceiveApps(
FlowyResult<List<ViewPB>, FlowyError> appsOrFail,
) = _ReceiveApps;
}
@freezed
class MenuState with _$MenuState {
const factory MenuState({
required List<ViewPB> views,
required FlowyResult<void, FlowyError> successOrFailure,
ViewPB? lastCreatedView,
}) = _MenuState;
factory MenuState.initial() => MenuState(
views: [],
successOrFailure: FlowyResult.success(null),
);
}

View File

@ -1,2 +1,2 @@
export 'menu_bloc.dart';
export 'menu_user_bloc.dart';
export 'sidebar_root_views_bloc.dart';

View File

@ -0,0 +1,160 @@
import 'dart:async';
import 'package:appflowy/workspace/application/workspace/workspace_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'sidebar_root_views_bloc.freezed.dart';
class SidebarRootViewsBloc
extends Bloc<SidebarRootViewsEvent, SidebarRootViewState> {
SidebarRootViewsBloc() : super(SidebarRootViewState.initial()) {
_dispatch();
}
late WorkspaceService _workspaceService;
WorkspaceListener? _listener;
@override
Future<void> close() async {
await _listener?.stop();
return super.close();
}
void _dispatch() {
on<SidebarRootViewsEvent>(
(event, emit) async {
await event.when(
initial: (userProfile, workspaceId) async {
_initial(userProfile, workspaceId);
await _fetchApps(emit);
},
reset: (userProfile, workspaceId) async {
await _listener?.stop();
_initial(userProfile, workspaceId);
await _fetchApps(emit);
},
createRootView: (name, desc, index) async {
final result = await _workspaceService.createApp(
name: name,
desc: desc,
index: index,
);
result.fold(
(view) => emit(state.copyWith(lastCreatedRootView: view)),
(error) {
Log.error(error);
emit(
state.copyWith(
successOrFailure: FlowyResult.failure(error),
),
);
},
);
},
didReceiveViews: (viewsOrFailure) async {
emit(
viewsOrFailure.fold(
(views) => state.copyWith(
views: views,
successOrFailure: FlowyResult.success(null),
),
(err) =>
state.copyWith(successOrFailure: FlowyResult.failure(err)),
),
);
},
moveRootView: (int fromIndex, int toIndex) {
if (state.views.length > fromIndex) {
final view = state.views[fromIndex];
_workspaceService.moveApp(
appId: view.id,
fromIndex: fromIndex,
toIndex: toIndex,
);
final views = List<ViewPB>.from(state.views);
views.insert(toIndex, views.removeAt(fromIndex));
emit(state.copyWith(views: views));
}
},
);
},
);
}
Future<void> _fetchApps(Emitter<SidebarRootViewState> emit) async {
final viewsOrError = await _workspaceService.getViews();
emit(
viewsOrError.fold(
(views) => state.copyWith(views: views),
(error) {
Log.error(error);
return state.copyWith(successOrFailure: FlowyResult.failure(error));
},
),
);
}
void _handleAppsOrFail(FlowyResult<List<ViewPB>, FlowyError> viewsOrFail) {
viewsOrFail.fold(
(views) => add(
SidebarRootViewsEvent.didReceiveViews(FlowyResult.success(views)),
),
(error) => add(
SidebarRootViewsEvent.didReceiveViews(FlowyResult.failure(error)),
),
);
}
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
_listener = WorkspaceListener(
user: userProfile,
workspaceId: workspaceId,
)..start(appsChanged: _handleAppsOrFail);
}
}
@freezed
class SidebarRootViewsEvent with _$SidebarRootViewsEvent {
const factory SidebarRootViewsEvent.initial(
UserProfilePB userProfile,
String workspaceId,
) = _Initial;
const factory SidebarRootViewsEvent.reset(
UserProfilePB userProfile,
String workspaceId,
) = _Reset;
const factory SidebarRootViewsEvent.createRootView(
String name, {
String? desc,
int? index,
}) = _createRootView;
const factory SidebarRootViewsEvent.moveRootView(int fromIndex, int toIndex) =
_MoveRootView;
const factory SidebarRootViewsEvent.didReceiveViews(
FlowyResult<List<ViewPB>, FlowyError> appsOrFail,
) = _ReceiveApps;
}
@freezed
class SidebarRootViewState with _$SidebarRootViewState {
const factory SidebarRootViewState({
required List<ViewPB> views,
required FlowyResult<void, FlowyError> successOrFailure,
@Default(null) ViewPB? lastCreatedRootView,
}) = _SidebarRootViewState;
factory SidebarRootViewState.initial() => SidebarRootViewState(
views: [],
successOrFailure: FlowyResult.success(null),
);
}

View File

@ -1,10 +1,13 @@
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'user_workspace_bloc.freezed.dart';
@ -33,43 +36,207 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
},
createWorkspace: (name, desc) async {
final result = await _userService.createUserWorkspace(name);
final (workspaces, createWorkspaceResult) = result.fold(
(s) {
final workspaces = [...state.workspaces, s];
return (
workspaces,
FlowyResult<void, FlowyError>.success(null)
);
},
(e) {
Log.error(e);
return (state.workspaces, FlowyResult.failure(e));
},
);
emit(
state.copyWith(
openWorkspaceResult: null,
deleteWorkspaceResult: null,
createWorkspaceResult:
result.fold((s) => FlowyResult.success(null), (e) {
Log.error(e);
return FlowyResult.failure(e);
}),
updateWorkspaceIconResult: null,
createWorkspaceResult: createWorkspaceResult,
workspaces: workspaces,
),
);
},
deleteWorkspace: (workspaceId) async {
if (state.workspaces.length <= 1) {
// do not allow to delete the last workspace
return emit(
state.copyWith(
openWorkspaceResult: null,
createWorkspaceResult: null,
updateWorkspaceIconResult: null,
renameWorkspaceResult: null,
deleteWorkspaceResult: FlowyResult.failure(
FlowyError(
code: ErrorCode.Internal,
msg: 'Cannot delete the last workspace',
),
),
),
);
}
final result = await _userService.deleteWorkspaceById(workspaceId);
final (workspaces, deleteWorkspaceResult) = result.fold(
(s) {
// if the current workspace is deleted, open the first workspace
if (state.currentWorkspace?.workspaceId == workspaceId) {
add(OpenWorkspace(state.workspaces.first.workspaceId));
}
// remove the deleted workspace from the list instead of fetching
// the workspaces again
final workspaces = [...state.workspaces]..removeWhere(
(e) => e.workspaceId == workspaceId,
);
return (
workspaces,
FlowyResult<void, FlowyError>.success(null)
);
},
(e) {
Log.error(e);
return (state.workspaces, FlowyResult.failure(e));
},
);
emit(
state.copyWith(
openWorkspaceResult: null,
createWorkspaceResult: null,
deleteWorkspaceResult:
result.fold((s) => FlowyResult.success(null), (e) {
Log.error(e);
return FlowyResult.failure(e);
}),
updateWorkspaceIconResult: null,
renameWorkspaceResult: null,
deleteWorkspaceResult: deleteWorkspaceResult,
workspaces: workspaces,
),
);
},
openWorkspace: (workspaceId) async {
final result = await _userService.openWorkspace(workspaceId);
final (currentWorkspace, openWorkspaceResult) =
await _userService.openWorkspace(workspaceId).fold(
(s) {
final openedWorkspace = state.workspaces.firstWhere(
(e) => e.workspaceId == workspaceId,
);
return (
openedWorkspace,
FlowyResult<void, FlowyError>.success(null)
);
},
(f) {
Log.error(f);
return (state.currentWorkspace, FlowyResult.failure(f));
},
);
emit(
state.copyWith(
createWorkspaceResult: null,
deleteWorkspaceResult: null,
openWorkspaceResult:
result.fold((s) => FlowyResult.success(null), (e) {
updateWorkspaceIconResult: null,
openWorkspaceResult: openWorkspaceResult,
currentWorkspace: currentWorkspace,
),
);
},
renameWorkspace: (workspaceId, name) async {
final result = await _userService.renameWorkspace(
workspaceId,
name,
);
final (workspaces, currentWorkspace, renameWorkspaceResult) =
result.fold(
(s) {
final workspaces = state.workspaces.map((e) {
if (e.workspaceId == workspaceId) {
e.freeze();
return e.rebuild((p0) {
p0.name = name;
});
}
return e;
}).toList();
final currentWorkspace = workspaces.firstWhere(
(e) => e.workspaceId == state.currentWorkspace?.workspaceId,
);
return (
workspaces,
currentWorkspace,
FlowyResult<void, FlowyError>.success(null),
);
},
(e) {
Log.error(e);
return FlowyResult.failure(e);
}),
return (
state.workspaces,
state.currentWorkspace,
FlowyResult.failure(e),
);
},
);
emit(
state.copyWith(
createWorkspaceResult: null,
deleteWorkspaceResult: null,
openWorkspaceResult: null,
updateWorkspaceIconResult: null,
workspaces: workspaces,
currentWorkspace: currentWorkspace,
renameWorkspaceResult: renameWorkspaceResult,
),
);
},
updateWorkspaceIcon: (workspaceId, icon) async {
final result = await _userService.updateWorkspaceIcon(
workspaceId,
icon,
);
final (workspaces, currentWorkspace, updateWorkspaceIconResult) =
result.fold(
(s) {
final workspaces = state.workspaces.map((e) {
if (e.workspaceId == workspaceId) {
e.freeze();
return e.rebuild((p0) {
// TODO(Lucas): the icon is not ready in the backend
});
}
return e;
}).toList();
final currentWorkspace = workspaces.firstWhere(
(e) => e.workspaceId == state.currentWorkspace?.workspaceId,
);
return (
workspaces,
currentWorkspace,
FlowyResult<void, FlowyError>.success(null),
);
},
(e) {
Log.error(e);
return (
state.workspaces,
state.currentWorkspace,
FlowyResult.failure(e),
);
},
);
emit(
state.copyWith(
createWorkspaceResult: null,
deleteWorkspaceResult: null,
openWorkspaceResult: null,
renameWorkspaceResult: null,
updateWorkspaceIconResult: updateWorkspaceIconResult,
workspaces: workspaces,
currentWorkspace: currentWorkspace,
),
);
},
@ -83,24 +250,17 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
Future<(UserWorkspacePB currentWorkspace, List<UserWorkspacePB> workspaces)?>
_fetchWorkspaces() async {
final result = await _userService.getCurrentWorkspace();
return result.fold((currentWorkspace) async {
final result = await _userService.getWorkspaces();
return result.fold((workspaces) {
return (
workspaces.firstWhere(
(e) => e.workspaceId == currentWorkspace.id,
),
workspaces
);
}, (e) {
try {
final currentWorkspace =
await _userService.getCurrentWorkspace().getOrThrow();
final workspaces = await _userService.getWorkspaces().getOrThrow();
final currentWorkspaceInList =
workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id);
return (currentWorkspaceInList, workspaces);
} catch (e) {
Log.error(e);
return null;
});
}, (e) {
Log.error(e);
return null;
});
}
}
}
@ -114,6 +274,14 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent {
DeleteWorkspace;
const factory UserWorkspaceEvent.openWorkspace(String workspaceId) =
OpenWorkspace;
const factory UserWorkspaceEvent.renameWorkspace(
String workspaceId,
String name,
) = _RenameWorkspace;
const factory UserWorkspaceEvent.updateWorkspaceIcon(
String workspaceId,
String icon,
) = _UpdateWorkspaceIcon;
const factory UserWorkspaceEvent.workspacesReceived(
FlowyResult<List<UserWorkspacePB>, FlowyError> workspacesOrFail,
) = WorkspacesReceived;
@ -127,6 +295,8 @@ class UserWorkspaceState with _$UserWorkspaceState {
@Default(null) FlowyResult<void, FlowyError>? createWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? deleteWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? openWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? renameWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? updateWorkspaceIconResult,
}) = _UserWorkspaceState;
factory UserWorkspaceState.initial() =>

View File

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
@ -125,8 +125,8 @@ class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
LocaleKeys.newPageText.tr(),
(viewName, _) {
if (viewName.isNotEmpty) {
context.read<MenuBloc>().add(
MenuEvent.createApp(
context.read<SidebarRootViewsBloc>().add(
SidebarRootViewsEvent.createRootView(
viewName,
index: 0,
),

View File

@ -1,14 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/application/notifications/notification_action.dart';
import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
@ -22,6 +21,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Home Sidebar is the left side bar of the home page.
@ -81,35 +81,60 @@ class _HomeSideBarState extends State<HomeSideBar> {
@override
Widget build(BuildContext context) {
return BlocProvider<UserWorkspaceBloc>(
create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile)
..add(const UserWorkspaceEvent.fetchWorkspaces()),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
builder: (context, state) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => getIt<NotificationActionBloc>(),
),
BlocProvider(
create: (_) => MenuBloc(
user: widget.userProfile,
workspaceId: widget.workspaceSetting.workspaceId,
)..add(const MenuEvent.initial()),
create: (_) => SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
widget.userProfile,
state.currentWorkspace?.workspaceId ??
widget.workspaceSetting.workspaceId,
),
),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<MenuBloc, MenuState>(
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
listenWhen: (p, c) =>
p.lastCreatedView?.id != c.lastCreatedView?.id,
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) => context.read<TabsBloc>().add(
TabsEvent.openPlugin(plugin: state.lastCreatedView!.plugin()),
TabsEvent.openPlugin(
plugin: state.lastCreatedRootView!.plugin(),
),
),
),
BlocListener<NotificationActionBloc, NotificationActionState>(
listenWhen: (_, curr) => curr.action != null,
listener: _onNotificationAction,
),
BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) {
context.read<SidebarRootViewsBloc>().add(
SidebarRootViewsEvent.reset(
widget.userProfile,
state.currentWorkspace?.workspaceId ??
widget.workspaceSetting.workspaceId,
),
);
},
),
],
child: Builder(
builder: (context) {
final menuState = context.watch<MenuBloc>().state;
final menuState = context.watch<SidebarRootViewsBloc>().state;
final favoriteState = context.watch<FavoriteBloc>().state;
return _buildSidebar(
@ -121,6 +146,9 @@ class _HomeSideBarState extends State<HomeSideBar> {
),
),
);
},
),
);
}
Widget _buildSidebar(
@ -195,8 +223,11 @@ class _HomeSideBarState extends State<HomeSideBar> {
final action = state.action;
if (action != null) {
if (action.type == ActionType.openView) {
final view =
context.read<MenuBloc>().state.views.findView(action.objectId);
final view = context
.read<SidebarRootViewsBloc>()
.state
.views
.findView(action.objectId);
if (view != null) {
final Map<String, dynamic> arguments = {};

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
@ -25,7 +25,9 @@ class SidebarNewPageButton extends StatelessWidget {
LocaleKeys.newPageText.tr(),
(viewName, _) {
if (viewName.isNotEmpty) {
context.read<MenuBloc>().add(MenuEvent.createApp(viewName));
context
.read<SidebarRootViewsBloc>()
.add(SidebarRootViewsEvent.createRootView(viewName));
}
},
),

View File

@ -4,7 +4,7 @@ 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/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -24,7 +24,7 @@ class SidebarTopMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<MenuBloc, MenuState>(
return BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
builder: (context, state) {
return SizedBox(
height: HomeSizes.topBarHeight,

View File

@ -3,7 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@ -13,6 +13,7 @@ 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:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarWorkspace extends StatelessWidget {
@ -27,10 +28,7 @@ class SidebarWorkspace extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<UserWorkspaceBloc>(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(const UserWorkspaceEvent.fetchWorkspaces()),
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
listener: _showResultDialog,
builder: (context, state) {
final currentWorkspace = state.currentWorkspace;
@ -52,7 +50,6 @@ class SidebarWorkspace extends StatelessWidget {
],
);
},
),
);
}
@ -86,6 +83,26 @@ class SidebarWorkspace extends StatelessWidget {
showSnackBarMessage(context, message);
return;
}
result = state.updateWorkspaceIconResult;
if (result != null) {
final message = result.fold(
(s) => LocaleKeys.workspace_updateIconSuccess.tr(),
(e) => '${LocaleKeys.workspace_updateIconFailed.tr()}: ${e.msg}',
);
showSnackBarMessage(context, message);
return;
}
result = state.renameWorkspaceResult;
if (result != null) {
final message = result.fold(
(s) => LocaleKeys.workspace_renameSuccess.tr(),
(e) => '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}',
);
showSnackBarMessage(context, message);
return;
}
}
}
@ -161,6 +178,7 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> {
},
child: FlowyButton(
onTap: () => controller.show(),
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(vertical: 8),
text: Row(
children: [
@ -170,10 +188,12 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> {
child: WorkspaceIcon(workspace: widget.currentWorkspace),
),
const HSpace(8),
FlowyText.medium(
Expanded(
child: FlowyText.medium(
widget.currentWorkspace.name,
overflow: TextOverflow.ellipsis,
),
),
const FlowySvg(FlowySvgs.drop_menu_show_m),
],
),

View File

@ -64,23 +64,34 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
),
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
onTap: () async {
PopoverContainer.of(context).closeAll();
final workspaceBloc = context.read<UserWorkspaceBloc>();
switch (inner) {
case WorkspaceMoreAction.delete:
await NavigatorAlertDialog(
title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
confirm: () {
context.read<UserWorkspaceBloc>().add(
workspaceBloc.add(
UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId),
);
},
).show(context);
case WorkspaceMoreAction.rename:
// TODO(Lucas): integrate with the backend
}
if (context.mounted) {
PopoverContainer.of(context).closeAll();
await NavigatorTextFieldDialog(
title: LocaleKeys.workspace_create.tr(),
value: workspace.name,
hintText: '',
autoSelectAllText: true,
onConfirm: (name, context) async {
workspaceBloc.add(
UserWorkspaceEvent.renameWorkspace(
workspace.workspaceId,
name,
),
);
},
).show(context);
}
},
);

View File

@ -1,7 +1,11 @@
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class WorkspaceIcon extends StatelessWidget {
const WorkspaceIcon({
@ -13,8 +17,26 @@ class WorkspaceIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO(Lucas): support icon later
return Container(
return AppFlowyPopover(
offset: const Offset(0, 8),
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(360, 380)),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (BuildContext popoverContext) {
return FlowyIconPicker(
onSelected: (result) {
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
);
},
);
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: ColorGenerator.generateColorFromString(workspace.name),
@ -25,6 +47,8 @@ class WorkspaceIcon extends StatelessWidget {
fontSize: 16,
color: Colors.black,
),
),
),
);
}
}

View File

@ -4,10 +4,8 @@ import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@ -15,6 +13,9 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@visibleForTesting
const createWorkspaceButtonKey = ValueKey('createWorkspaceButton');
class WorkspacesMenu extends StatelessWidget {
const WorkspacesMenu({
super.key,
@ -38,14 +39,17 @@ class WorkspacesMenu extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
children: [
FlowyText.medium(
Expanded(
child: FlowyText.medium(
_getUserInfo(),
fontSize: 12.0,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
const Spacer(),
),
const HSpace(4.0),
FlowyButton(
key: createWorkspaceButtonKey,
useIntrinsicWidth: true,
text: const FlowySvg(FlowySvgs.add_m),
onTap: () {
@ -57,7 +61,7 @@ class WorkspacesMenu extends StatelessWidget {
),
),
for (final workspace in workspaces) ...[
_WorkspaceMenuItem(
WorkspaceMenuItem(
workspace: workspace,
userProfile: userProfile,
isSelected: workspace.workspaceId == currentWorkspace.workspaceId,
@ -82,29 +86,19 @@ class WorkspacesMenu extends StatelessWidget {
Future<void> _showCreateWorkspaceDialog(BuildContext context) async {
if (context.mounted) {
await NavigatorTextFieldDialog(
title: LocaleKeys.workspace_create.tr(),
value: '',
hintText: '',
autoSelectAllText: true,
onConfirm: (name, context) async {
final request = CreateWorkspacePB.create()..name = name;
final result = await UserEventCreateWorkspace(request).send();
final message = result.fold(
(s) => LocaleKeys.workspace_createSuccess.tr(),
(e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}',
);
if (context.mounted) {
showSnackBarMessage(context, message);
}
final workspaceBloc = context.read<UserWorkspaceBloc>();
await CreateWorkspaceDialog(
onConfirm: (name) {
workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name, ''));
},
).show(context);
}
}
}
class _WorkspaceMenuItem extends StatelessWidget {
const _WorkspaceMenuItem({
class WorkspaceMenuItem extends StatelessWidget {
const WorkspaceMenuItem({
super.key,
required this.workspace,
required this.userProfile,
required this.isSelected,
@ -143,9 +137,10 @@ class _WorkspaceMenuItem extends StatelessWidget {
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
iconPadding: 10.0,
leftIconSize: const Size.square(32),
leftIcon: WorkspaceIcon(
workspace: workspace,
leftIcon: const SizedBox.square(
dimension: 32,
),
rightIcon: const HSpace(42.0),
text: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -163,6 +158,15 @@ class _WorkspaceMenuItem extends StatelessWidget {
],
),
),
Positioned(
left: 12,
child: SizedBox.square(
dimension: 32,
child: WorkspaceIcon(
workspace: workspace,
),
),
),
Positioned(
right: 12.0,
child: Align(child: _buildRightIcon(context)),
@ -187,9 +191,28 @@ class _WorkspaceMenuItem extends StatelessWidget {
WorkspaceMoreActionList(workspace: workspace),
const FlowySvg(
FlowySvgs.blue_check_s,
blendMode: null,
),
],
);
}
}
class CreateWorkspaceDialog extends StatelessWidget {
const CreateWorkspaceDialog({
super.key,
required this.onConfirm,
});
final void Function(String name) onConfirm;
@override
Widget build(BuildContext context) {
return NavigatorTextFieldDialog(
title: LocaleKeys.workspace_create.tr(),
value: '',
hintText: '',
autoSelectAllText: true,
onConfirm: (name, _) => onConfirm(name),
);
}
}

View File

@ -1,3 +1,4 @@
library appflowy_result;
export 'src/async_result.dart';
export 'src/result.dart';

View File

@ -0,0 +1,33 @@
import 'package:appflowy_result/appflowy_result.dart';
typedef FlowyAsyncResult<S, F extends Object> = Future<FlowyResult<S, F>>;
extension FlowyAsyncResultExtension<S, F extends Object>
on FlowyAsyncResult<S, F> {
Future<S> getOrElse(S Function(F f) onFailure) {
return then((result) => result.getOrElse(onFailure));
}
Future<S> getOrThrow() {
return then((result) => result.getOrThrow());
}
Future<W> fold<W>(
W Function(S s) onSuccess,
W Function(F f) onFailure,
) {
return then<W>((result) => result.fold(onSuccess, onFailure));
}
Future<bool> isError() {
return then((result) => result.isFailure());
}
Future<bool> isSuccess() {
return then((result) => result.isSuccess());
}
FlowyAsyncResult<S, F> onFailure(void Function(F failure) onFailure) {
return then((result) => result..onFailure(onFailure));
}
}

View File

@ -1,30 +1,28 @@
abstract class FlowyResult<S, F> {
abstract class FlowyResult<S, F extends Object> {
const FlowyResult();
factory FlowyResult.success(S s) => FlowySuccess(s);
factory FlowyResult.failure(F e) => FlowyFailure(e);
factory FlowyResult.failure(F f) => FlowyFailure(f);
T fold<T>(T Function(S s) onSuccess, T Function(F e) onFailure);
T fold<T>(T Function(S s) onSuccess, T Function(F f) onFailure);
FlowyResult<T, F> map<T>(T Function(S success) fn);
FlowyResult<S, T> mapError<T>(T Function(F error) fn);
FlowyResult<S, T> mapError<T extends Object>(T Function(F failure) fn);
bool isSuccess();
bool isFailure();
S? toNullable();
void onSuccess(
void Function(S s) onSuccess,
);
void onSuccess(void Function(S s) onSuccess);
void onFailure(void Function(F f) onFailure);
void onFailure(
void Function(F f) onFailure,
);
S getOrElse(S Function(F failure) onFailure);
S getOrThrow();
}
class FlowySuccess<S, F> implements FlowyResult<S, F> {
class FlowySuccess<S, F extends Object> implements FlowyResult<S, F> {
final S _value;
FlowySuccess(this._value);
@ -54,7 +52,7 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
}
@override
FlowyResult<S, T> mapError<T>(T Function(F error) fn) {
FlowyResult<S, T> mapError<T extends Object>(T Function(F error) fn) {
return FlowySuccess(_value);
}
@ -80,40 +78,50 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
@override
void onFailure(void Function(F failure) onFailure) {}
@override
S getOrElse(S Function(F failure) onFailure) {
return _value;
}
@override
S getOrThrow() {
return _value;
}
}
class FlowyFailure<S, F> implements FlowyResult<S, F> {
final F _error;
class FlowyFailure<S, F extends Object> implements FlowyResult<S, F> {
final F _value;
FlowyFailure(this._error);
FlowyFailure(this._value);
F get error => _error;
F get error => _value;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlowyFailure &&
runtimeType == other.runtimeType &&
_error == other._error;
_value == other._value;
@override
int get hashCode => _error.hashCode;
int get hashCode => _value.hashCode;
@override
String toString() => 'Failure(error: $_error)';
String toString() => 'Failure(error: $_value)';
@override
T fold<T>(T Function(S s) onSuccess, T Function(F e) onFailure) =>
onFailure(_error);
onFailure(_value);
@override
map<T>(T Function(S success) fn) {
return FlowyFailure(_error);
return FlowyFailure(_value);
}
@override
FlowyResult<S, T> mapError<T>(T Function(F error) fn) {
return FlowyFailure(fn(_error));
FlowyResult<S, T> mapError<T extends Object>(T Function(F error) fn) {
return FlowyFailure(fn(_value));
}
@override
@ -136,6 +144,16 @@ class FlowyFailure<S, F> implements FlowyResult<S, F> {
@override
void onFailure(void Function(F failure) onFailure) {
onFailure(_error);
onFailure(_value);
}
@override
S getOrElse(S Function(F failure) onFailure) {
return onFailure(_value);
}
@override
S getOrThrow() {
throw _value;
}
}

View File

@ -1,4 +1,4 @@
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util.dart';
@ -10,26 +10,33 @@ void main() {
});
test('assert initial apps is the build-in app', () async {
final menuBloc = MenuBloc(
user: testContext.userProfile,
workspaceId: testContext.currentWorkspace.id,
)..add(const MenuEvent.initial());
final menuBloc = SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture();
assert(menuBloc.state.views.length == 1);
});
test('reorder apps', () async {
final menuBloc = MenuBloc(
user: testContext.userProfile,
workspaceId: testContext.currentWorkspace.id,
)..add(const MenuEvent.initial());
final menuBloc = SidebarRootViewsBloc()
..add(
SidebarRootViewsEvent.initial(
testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture();
menuBloc.add(const MenuEvent.createApp("App 1"));
menuBloc.add(const SidebarRootViewsEvent.createRootView("App 1"));
await blocResponseFuture();
menuBloc.add(const MenuEvent.createApp("App 2"));
menuBloc.add(const SidebarRootViewsEvent.createRootView("App 2"));
await blocResponseFuture();
menuBloc.add(const MenuEvent.createApp("App 3"));
menuBloc.add(const SidebarRootViewsEvent.createRootView("App 3"));
await blocResponseFuture();
assert(menuBloc.state.views[1].name == 'App 1');

View File

@ -71,7 +71,11 @@
"deleteSuccess": "Workspace deleted successfully",
"deleteFailed": "Failed to delete workspace",
"openSuccess": "Open workspace successfully",
"openFailed": "Failed to open workspace"
"openFailed": "Failed to open workspace",
"renameSuccess": "Workspace renamed successfully",
"renameFailed": "Failed to rename workspace",
"updateIconSuccess": "Workspace reset successfully",
"updateIconFailed": "Failed to reset workspace"
},
"shareAction": {
"buttonText": "Share",