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 '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 'document_sync_test.dart' as document_sync_test;
import 'user_setting_sync_test.dart' as user_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 { Future<void> main() async {
preset_af_cloud_env_test.main(); preset_af_cloud_env_test.main();
@ -14,4 +15,6 @@ Future<void> main() async {
user_sync_test.main(); user_sync_test.main();
anon_user_continue_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( Future<void> pumpUntilFound(
Finder finder, { Finder finder, {
Duration timeout = const Duration(seconds: 10), Duration timeout = const Duration(seconds: 10),
Duration pumpInterval = Duration pumpInterval = const Duration(
const Duration(milliseconds: 50), // Interval between pumps milliseconds: 50,
), // Interval between pumps
}) async { }) async {
bool timerDone = false; bool timerDone = false;
final timer = Timer(timeout, () => timerDone = true); final timer = Timer(timeout, () => timerDone = true);
@ -148,6 +149,24 @@ extension AppFlowyTestBase on WidgetTester {
timer.cancel(); 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( Future<void> tapButton(
Finder finder, { Finder finder, {
int? pointer, 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/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.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/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy/user/presentation/screens/screens.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.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_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/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_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
@ -518,6 +521,51 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle(); 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 { 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_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
class NotificationParser<T, E> { class NotificationParser<T, E extends Object> {
NotificationParser({ NotificationParser({
this.id, this.id,
required this.callback, 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/home/favorite_folder/mobile_home_favorite_folder.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.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/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-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -27,10 +27,13 @@ class MobileFavoritePageFolder extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (_) => MenuBloc( create: (_) => SidebarRootViewsBloc()
user: userProfile, ..add(
workspaceId: workspaceSetting.workspaceId, SidebarRootViewsEvent.initial(
)..add(const MenuEvent.initial()), userProfile,
workspaceSetting.workspaceId,
),
),
), ),
BlocProvider( BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
@ -38,11 +41,11 @@ class MobileFavoritePageFolder extends StatelessWidget {
], ],
child: MultiBlocListener( child: MultiBlocListener(
listeners: [ listeners: [
BlocListener<MenuBloc, MenuState>( BlocListener<SidebarRootViewsBloc, SidebarRootViewState>(
listenWhen: (p, c) => listenWhen: (p, c) =>
p.lastCreatedView?.id != c.lastCreatedView?.id, p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) => listener: (context, state) =>
context.pushView(state.lastCreatedView!), context.pushView(state.lastCreatedRootView!),
), ),
], ],
child: Builder( child: Builder(

View File

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

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -67,8 +67,8 @@ class _MobilePersonalFolderHeaderState
size: Size.square(iconSize), size: Size.square(iconSize),
), ),
onPressed: () { onPressed: () {
context.read<MenuBloc>().add( context.read<SidebarRootViewsBloc>().add(
MenuEvent.createApp( SidebarRootViewsEvent.createRootView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(), LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0, 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/startup/startup.dart';
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_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/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart'; import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
@ -80,11 +80,14 @@ class _NotificationScreenContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => MenuBloc( create: (_) => SidebarRootViewsBloc()
workspaceId: workspaceSetting.workspaceId, ..add(
user: userProfile, SidebarRootViewsEvent.initial(
)..add(const MenuEvent.initial()), userProfile,
child: BlocBuilder<MenuBloc, MenuState>( workspaceSetting.workspaceId,
),
),
child: BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
builder: (context, menuState) => builder: (context, menuState) =>
BlocBuilder<NotificationFilterBloc, NotificationFilterState>( BlocBuilder<NotificationFilterBloc, NotificationFilterState>(
builder: (context, filterState) => builder: (context, filterState) =>

View File

@ -130,4 +130,24 @@ class UserBackendService {
final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId; final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
return UserEventDeleteWorkspace(request).send(); 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 '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/user/application/user_service.dart';
import 'package:appflowy_backend/log.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-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'user_workspace_bloc.freezed.dart'; part 'user_workspace_bloc.freezed.dart';
@ -33,43 +36,207 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
}, },
createWorkspace: (name, desc) async { createWorkspace: (name, desc) async {
final result = await _userService.createUserWorkspace(name); 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( emit(
state.copyWith( state.copyWith(
openWorkspaceResult: null, openWorkspaceResult: null,
deleteWorkspaceResult: null, deleteWorkspaceResult: null,
createWorkspaceResult: updateWorkspaceIconResult: null,
result.fold((s) => FlowyResult.success(null), (e) { createWorkspaceResult: createWorkspaceResult,
Log.error(e); workspaces: workspaces,
return FlowyResult.failure(e);
}),
), ),
); );
}, },
deleteWorkspace: (workspaceId) async { 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 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( emit(
state.copyWith( state.copyWith(
openWorkspaceResult: null, openWorkspaceResult: null,
createWorkspaceResult: null, createWorkspaceResult: null,
deleteWorkspaceResult: updateWorkspaceIconResult: null,
result.fold((s) => FlowyResult.success(null), (e) { renameWorkspaceResult: null,
Log.error(e); deleteWorkspaceResult: deleteWorkspaceResult,
return FlowyResult.failure(e); workspaces: workspaces,
}),
), ),
); );
}, },
openWorkspace: (workspaceId) async { 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( emit(
state.copyWith( state.copyWith(
createWorkspaceResult: null, createWorkspaceResult: null,
deleteWorkspaceResult: null, deleteWorkspaceResult: null,
openWorkspaceResult: updateWorkspaceIconResult: null,
result.fold((s) => FlowyResult.success(null), (e) { openWorkspaceResult: openWorkspaceResult,
Log.error(e); currentWorkspace: currentWorkspace,
return FlowyResult.failure(e); ),
}), );
},
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)?> Future<(UserWorkspacePB currentWorkspace, List<UserWorkspacePB> workspaces)?>
_fetchWorkspaces() async { _fetchWorkspaces() async {
final result = await _userService.getCurrentWorkspace(); try {
return result.fold((currentWorkspace) async { final currentWorkspace =
final result = await _userService.getWorkspaces(); await _userService.getCurrentWorkspace().getOrThrow();
return result.fold((workspaces) { final workspaces = await _userService.getWorkspaces().getOrThrow();
return ( final currentWorkspaceInList =
workspaces.firstWhere( workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id);
(e) => e.workspaceId == currentWorkspace.id, return (currentWorkspaceInList, workspaces);
), } catch (e) {
workspaces
);
}, (e) {
Log.error(e);
return null;
});
}, (e) {
Log.error(e); Log.error(e);
return null; return null;
}); }
} }
} }
@ -114,6 +274,14 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent {
DeleteWorkspace; DeleteWorkspace;
const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = const factory UserWorkspaceEvent.openWorkspace(String workspaceId) =
OpenWorkspace; OpenWorkspace;
const factory UserWorkspaceEvent.renameWorkspace(
String workspaceId,
String name,
) = _RenameWorkspace;
const factory UserWorkspaceEvent.updateWorkspaceIcon(
String workspaceId,
String icon,
) = _UpdateWorkspaceIcon;
const factory UserWorkspaceEvent.workspacesReceived( const factory UserWorkspaceEvent.workspacesReceived(
FlowyResult<List<UserWorkspacePB>, FlowyError> workspacesOrFail, FlowyResult<List<UserWorkspacePB>, FlowyError> workspacesOrFail,
) = WorkspacesReceived; ) = WorkspacesReceived;
@ -127,8 +295,10 @@ class UserWorkspaceState with _$UserWorkspaceState {
@Default(null) FlowyResult<void, FlowyError>? createWorkspaceResult, @Default(null) FlowyResult<void, FlowyError>? createWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? deleteWorkspaceResult, @Default(null) FlowyResult<void, FlowyError>? deleteWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? openWorkspaceResult, @Default(null) FlowyResult<void, FlowyError>? openWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? renameWorkspaceResult,
@Default(null) FlowyResult<void, FlowyError>? updateWorkspaceIconResult,
}) = _UserWorkspaceState; }) = _UserWorkspaceState;
factory UserWorkspaceState.initial() => factory UserWorkspaceState.initial() =>
const UserWorkspaceState(currentWorkspace: null, workspaces: []); const UserWorkspaceState(currentWorkspace: null, workspaces: []);
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
@ -25,7 +25,9 @@ class SidebarNewPageButton extends StatelessWidget {
LocaleKeys.newPageText.tr(), LocaleKeys.newPageText.tr(),
(viewName, _) { (viewName, _) {
if (viewName.isNotEmpty) { 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/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.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/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:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -24,7 +24,7 @@ class SidebarTopMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<MenuBloc, MenuState>( return BlocBuilder<SidebarRootViewsBloc, SidebarRootViewState>(
builder: (context, state) { builder: (context, state) {
return SizedBox( return SizedBox(
height: HomeSizes.topBarHeight, 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/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/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_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/home/toast.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarWorkspace extends StatelessWidget { class SidebarWorkspace extends StatelessWidget {
@ -27,32 +28,28 @@ class SidebarWorkspace extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<UserWorkspaceBloc>( return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
create: (_) => UserWorkspaceBloc(userProfile: userProfile) listener: _showResultDialog,
..add(const UserWorkspaceEvent.fetchWorkspaces()), builder: (context, state) {
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>( final currentWorkspace = state.currentWorkspace;
listener: _showResultDialog, // todo: show something if there is no workspace
builder: (context, state) { if (currentWorkspace == null) {
final currentWorkspace = state.currentWorkspace; return const SizedBox.shrink();
// todo: show something if there is no workspace }
if (currentWorkspace == null) { return Row(
return const SizedBox.shrink(); children: [
} Expanded(
return Row( child: _WorkspaceWrapper(
children: [ userProfile: userProfile,
Expanded( currentWorkspace: currentWorkspace,
child: _WorkspaceWrapper(
userProfile: userProfile,
currentWorkspace: currentWorkspace,
),
), ),
UserSettingButton(userProfile: userProfile), ),
const HSpace(4), UserSettingButton(userProfile: userProfile),
NotificationButton(views: views), const HSpace(4),
], NotificationButton(views: views),
); ],
}, );
), },
); );
} }
@ -86,6 +83,26 @@ class SidebarWorkspace extends StatelessWidget {
showSnackBarMessage(context, message); showSnackBarMessage(context, message);
return; 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( child: FlowyButton(
onTap: () => controller.show(), onTap: () => controller.show(),
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
text: Row( text: Row(
children: [ children: [
@ -170,9 +188,11 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> {
child: WorkspaceIcon(workspace: widget.currentWorkspace), child: WorkspaceIcon(workspace: widget.currentWorkspace),
), ),
const HSpace(8), const HSpace(8),
FlowyText.medium( Expanded(
widget.currentWorkspace.name, child: FlowyText.medium(
overflow: TextOverflow.ellipsis, widget.currentWorkspace.name,
overflow: TextOverflow.ellipsis,
),
), ),
const FlowySvg(FlowySvgs.drop_menu_show_m), 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), margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
onTap: () async { onTap: () async {
PopoverContainer.of(context).closeAll();
final workspaceBloc = context.read<UserWorkspaceBloc>();
switch (inner) { switch (inner) {
case WorkspaceMoreAction.delete: case WorkspaceMoreAction.delete:
await NavigatorAlertDialog( await NavigatorAlertDialog(
title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
confirm: () { confirm: () {
context.read<UserWorkspaceBloc>().add( workspaceBloc.add(
UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId),
); );
}, },
).show(context); ).show(context);
case WorkspaceMoreAction.rename: case WorkspaceMoreAction.rename:
await NavigatorTextFieldDialog(
// TODO(Lucas): integrate with the backend title: LocaleKeys.workspace_create.tr(),
} value: workspace.name,
hintText: '',
if (context.mounted) { autoSelectAllText: true,
PopoverContainer.of(context).closeAll(); 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/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_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:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class WorkspaceIcon extends StatelessWidget { class WorkspaceIcon extends StatelessWidget {
const WorkspaceIcon({ const WorkspaceIcon({
@ -13,17 +17,37 @@ class WorkspaceIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO(Lucas): support icon later return AppFlowyPopover(
return Container( offset: const Offset(0, 8),
alignment: Alignment.center, direction: PopoverDirection.bottomWithLeftAligned,
decoration: BoxDecoration( constraints: BoxConstraints.loose(const Size(360, 380)),
color: ColorGenerator.generateColorFromString(workspace.name), clickHandler: PopoverClickHandler.gestureDetector,
borderRadius: BorderRadius.circular(4), popupBuilder: (BuildContext popoverContext) {
), return FlowyIconPicker(
child: FlowyText( onSelected: (result) {
workspace.name.isEmpty ? '' : workspace.name.substring(0, 1), context.read<UserWorkspaceBloc>().add(
fontSize: 16, UserWorkspaceEvent.updateWorkspaceIcon(
color: Colors.black, 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,
),
),
), ),
); );
} }

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/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_actions.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_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/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@visibleForTesting
const createWorkspaceButtonKey = ValueKey('createWorkspaceButton');
class WorkspacesMenu extends StatelessWidget { class WorkspacesMenu extends StatelessWidget {
const WorkspacesMenu({ const WorkspacesMenu({
super.key, super.key,
@ -38,14 +39,17 @@ class WorkspacesMenu extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row( child: Row(
children: [ children: [
FlowyText.medium( Expanded(
_getUserInfo(), child: FlowyText.medium(
fontSize: 12.0, _getUserInfo(),
overflow: TextOverflow.ellipsis, fontSize: 12.0,
color: Theme.of(context).hintColor, overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),
), ),
const Spacer(), const HSpace(4.0),
FlowyButton( FlowyButton(
key: createWorkspaceButtonKey,
useIntrinsicWidth: true, useIntrinsicWidth: true,
text: const FlowySvg(FlowySvgs.add_m), text: const FlowySvg(FlowySvgs.add_m),
onTap: () { onTap: () {
@ -57,7 +61,7 @@ class WorkspacesMenu extends StatelessWidget {
), ),
), ),
for (final workspace in workspaces) ...[ for (final workspace in workspaces) ...[
_WorkspaceMenuItem( WorkspaceMenuItem(
workspace: workspace, workspace: workspace,
userProfile: userProfile, userProfile: userProfile,
isSelected: workspace.workspaceId == currentWorkspace.workspaceId, isSelected: workspace.workspaceId == currentWorkspace.workspaceId,
@ -82,29 +86,19 @@ class WorkspacesMenu extends StatelessWidget {
Future<void> _showCreateWorkspaceDialog(BuildContext context) async { Future<void> _showCreateWorkspaceDialog(BuildContext context) async {
if (context.mounted) { if (context.mounted) {
await NavigatorTextFieldDialog( final workspaceBloc = context.read<UserWorkspaceBloc>();
title: LocaleKeys.workspace_create.tr(), await CreateWorkspaceDialog(
value: '', onConfirm: (name) {
hintText: '', workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name, ''));
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);
}
}, },
).show(context); ).show(context);
} }
} }
} }
class _WorkspaceMenuItem extends StatelessWidget { class WorkspaceMenuItem extends StatelessWidget {
const _WorkspaceMenuItem({ const WorkspaceMenuItem({
super.key,
required this.workspace, required this.workspace,
required this.userProfile, required this.userProfile,
required this.isSelected, required this.isSelected,
@ -143,9 +137,10 @@ class _WorkspaceMenuItem extends StatelessWidget {
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
iconPadding: 10.0, iconPadding: 10.0,
leftIconSize: const Size.square(32), leftIconSize: const Size.square(32),
leftIcon: WorkspaceIcon( leftIcon: const SizedBox.square(
workspace: workspace, dimension: 32,
), ),
rightIcon: const HSpace(42.0),
text: Column( text: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -163,6 +158,15 @@ class _WorkspaceMenuItem extends StatelessWidget {
], ],
), ),
), ),
Positioned(
left: 12,
child: SizedBox.square(
dimension: 32,
child: WorkspaceIcon(
workspace: workspace,
),
),
),
Positioned( Positioned(
right: 12.0, right: 12.0,
child: Align(child: _buildRightIcon(context)), child: Align(child: _buildRightIcon(context)),
@ -187,9 +191,28 @@ class _WorkspaceMenuItem extends StatelessWidget {
WorkspaceMoreActionList(workspace: workspace), WorkspaceMoreActionList(workspace: workspace),
const FlowySvg( const FlowySvg(
FlowySvgs.blue_check_s, 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; library appflowy_result;
export 'src/async_result.dart';
export 'src/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(); const FlowyResult();
factory FlowyResult.success(S s) => FlowySuccess(s); 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<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 isSuccess();
bool isFailure(); bool isFailure();
S? toNullable(); S? toNullable();
void onSuccess( void onSuccess(void Function(S s) onSuccess);
void Function(S s) onSuccess, void onFailure(void Function(F f) onFailure);
);
void onFailure( S getOrElse(S Function(F failure) onFailure);
void Function(F f) onFailure, S getOrThrow();
);
} }
class FlowySuccess<S, F> implements FlowyResult<S, F> { class FlowySuccess<S, F extends Object> implements FlowyResult<S, F> {
final S _value; final S _value;
FlowySuccess(this._value); FlowySuccess(this._value);
@ -54,7 +52,7 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
} }
@override @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); return FlowySuccess(_value);
} }
@ -80,40 +78,50 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
@override @override
void onFailure(void Function(F failure) onFailure) {} 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> { class FlowyFailure<S, F extends Object> implements FlowyResult<S, F> {
final F _error; final F _value;
FlowyFailure(this._error); FlowyFailure(this._value);
F get error => _error; F get error => _value;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is FlowyFailure && other is FlowyFailure &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
_error == other._error; _value == other._value;
@override @override
int get hashCode => _error.hashCode; int get hashCode => _value.hashCode;
@override @override
String toString() => 'Failure(error: $_error)'; String toString() => 'Failure(error: $_value)';
@override @override
T fold<T>(T Function(S s) onSuccess, T Function(F e) onFailure) => T fold<T>(T Function(S s) onSuccess, T Function(F e) onFailure) =>
onFailure(_error); onFailure(_value);
@override @override
map<T>(T Function(S success) fn) { map<T>(T Function(S success) fn) {
return FlowyFailure(_error); return FlowyFailure(_value);
} }
@override @override
FlowyResult<S, T> mapError<T>(T Function(F error) fn) { FlowyResult<S, T> mapError<T extends Object>(T Function(F error) fn) {
return FlowyFailure(fn(_error)); return FlowyFailure(fn(_value));
} }
@override @override
@ -136,6 +144,16 @@ class FlowyFailure<S, F> implements FlowyResult<S, F> {
@override @override
void onFailure(void Function(F failure) onFailure) { 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 'package:flutter_test/flutter_test.dart';
import '../../util.dart'; import '../../util.dart';
@ -10,26 +10,33 @@ void main() {
}); });
test('assert initial apps is the build-in app', () async { test('assert initial apps is the build-in app', () async {
final menuBloc = MenuBloc( final menuBloc = SidebarRootViewsBloc()
user: testContext.userProfile, ..add(
workspaceId: testContext.currentWorkspace.id, SidebarRootViewsEvent.initial(
)..add(const MenuEvent.initial()); testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture(); await blocResponseFuture();
assert(menuBloc.state.views.length == 1); assert(menuBloc.state.views.length == 1);
}); });
test('reorder apps', () async { test('reorder apps', () async {
final menuBloc = MenuBloc( final menuBloc = SidebarRootViewsBloc()
user: testContext.userProfile, ..add(
workspaceId: testContext.currentWorkspace.id, SidebarRootViewsEvent.initial(
)..add(const MenuEvent.initial()); testContext.userProfile,
testContext.currentWorkspace.id,
),
);
await blocResponseFuture(); await blocResponseFuture();
menuBloc.add(const MenuEvent.createApp("App 1")); menuBloc.add(const SidebarRootViewsEvent.createRootView("App 1"));
await blocResponseFuture(); await blocResponseFuture();
menuBloc.add(const MenuEvent.createApp("App 2")); menuBloc.add(const SidebarRootViewsEvent.createRootView("App 2"));
await blocResponseFuture(); await blocResponseFuture();
menuBloc.add(const MenuEvent.createApp("App 3")); menuBloc.add(const SidebarRootViewsEvent.createRootView("App 3"));
await blocResponseFuture(); await blocResponseFuture();
assert(menuBloc.state.views[1].name == 'App 1'); assert(menuBloc.state.views[1].name == 'App 1');

View File

@ -71,7 +71,11 @@
"deleteSuccess": "Workspace deleted successfully", "deleteSuccess": "Workspace deleted successfully",
"deleteFailed": "Failed to delete workspace", "deleteFailed": "Failed to delete workspace",
"openSuccess": "Open workspace successfully", "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": { "shareAction": {
"buttonText": "Share", "buttonText": "Share",