mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
c0210a5778
commit
c8e86f4f26
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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: [
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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) =>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
export 'menu_bloc.dart';
|
||||
export 'menu_user_bloc.dart';
|
||||
export 'sidebar_root_views_bloc.dart';
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
@ -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) {
|
||||
Log.error(e);
|
||||
return FlowyResult.failure(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 (
|
||||
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) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
});
|
||||
}, (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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,8 +295,10 @@ 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() =>
|
||||
factory UserWorkspaceState.initial() =>
|
||||
const UserWorkspaceState(currentWorkspace: null, workspaces: []);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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,44 +81,72 @@ class _HomeSideBarState extends State<HomeSideBar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => getIt<NotificationActionBloc>(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => MenuBloc(
|
||||
user: widget.userProfile,
|
||||
workspaceId: widget.workspaceSetting.workspaceId,
|
||||
)..add(const MenuEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<MenuBloc, MenuState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedView?.id != c.lastCreatedView?.id,
|
||||
listener: (context, state) => context.read<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(plugin: state.lastCreatedView!.plugin()),
|
||||
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: (_) => SidebarRootViewsBloc()
|
||||
..add(
|
||||
SidebarRootViewsEvent.initial(
|
||||
widget.userProfile,
|
||||
state.currentWorkspace?.workspaceId ??
|
||||
widget.workspaceSetting.workspaceId,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
|
||||
listener: (context, state) => context.read<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: state.lastCreatedRootView!.plugin(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocListener<NotificationActionBloc, NotificationActionState>(
|
||||
listenWhen: (_, curr) => curr.action != null,
|
||||
listener: _onNotificationAction,
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final menuState = context.watch<MenuBloc>().state;
|
||||
final favoriteState = context.watch<FavoriteBloc>().state;
|
||||
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<SidebarRootViewsBloc>().state;
|
||||
final favoriteState = context.watch<FavoriteBloc>().state;
|
||||
|
||||
return _buildSidebar(
|
||||
context,
|
||||
menuState.views,
|
||||
favoriteState.views,
|
||||
);
|
||||
},
|
||||
),
|
||||
return _buildSidebar(
|
||||
context,
|
||||
menuState.views,
|
||||
favoriteState.views,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -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 = {};
|
||||
|
@ -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));
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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,32 +28,28 @@ class SidebarWorkspace extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<UserWorkspaceBloc>(
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(const UserWorkspaceEvent.fetchWorkspaces()),
|
||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: _showResultDialog,
|
||||
builder: (context, state) {
|
||||
final currentWorkspace = state.currentWorkspace;
|
||||
// todo: show something if there is no workspace
|
||||
if (currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _WorkspaceWrapper(
|
||||
userProfile: userProfile,
|
||||
currentWorkspace: currentWorkspace,
|
||||
),
|
||||
return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
listener: _showResultDialog,
|
||||
builder: (context, state) {
|
||||
final currentWorkspace = state.currentWorkspace;
|
||||
// todo: show something if there is no workspace
|
||||
if (currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _WorkspaceWrapper(
|
||||
userProfile: userProfile,
|
||||
currentWorkspace: currentWorkspace,
|
||||
),
|
||||
UserSettingButton(userProfile: userProfile),
|
||||
const HSpace(4),
|
||||
NotificationButton(views: views),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
UserSettingButton(userProfile: userProfile),
|
||||
const HSpace(4),
|
||||
NotificationButton(views: views),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,9 +188,11 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> {
|
||||
child: WorkspaceIcon(workspace: widget.currentWorkspace),
|
||||
),
|
||||
const HSpace(8),
|
||||
FlowyText.medium(
|
||||
widget.currentWorkspace.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
widget.currentWorkspace.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const FlowySvg(FlowySvgs.drop_menu_show_m),
|
||||
],
|
||||
|
@ -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(
|
||||
UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId),
|
||||
);
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -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,17 +17,37 @@ class WorkspaceIcon extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO(Lucas): support icon later
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorGenerator.generateColorFromString(workspace.name),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowyText(
|
||||
workspace.name.isEmpty ? '' : workspace.name.substring(0, 1),
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
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),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FlowyText(
|
||||
workspace.name.isEmpty ? '' : workspace.name.substring(0, 1),
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
_getUserInfo(),
|
||||
fontSize: 12.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).hintColor,
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
library appflowy_result;
|
||||
|
||||
export 'src/async_result.dart';
|
||||
export 'src/result.dart';
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user