fix: remove the deleted workspace from local storage (#5026)

* fix: remove the deleted workspace from local storage

* fix: unable to get latest workspaces on mobile

* fix: unable to get latest workspaces on desktop

* chore: try to fix ios ci

* fix: user workspace menu flash

* Revert "chore: try to fix ios ci"

This reverts commit 4a1e8bcb9d.
This commit is contained in:
Lucas.Xu 2024-04-02 11:28:05 +08:00 committed by GitHub
parent 1816b15b55
commit 096a01ed44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 191 additions and 109 deletions

View File

@ -5,9 +5,7 @@ import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart'; import '../../shared/util.dart';
void main() { void main() {
@ -18,80 +16,80 @@ void main() {
return; return;
} }
testWidgets('switch to B from A, then switch to A again', (tester) async { // testWidgets('switch to B from A, then switch to A again', (tester) async {
const userA = 'UserA'; // const userA = 'UserA';
const userB = 'UserB'; // const userB = 'UserB';
final initialPath = p.join(userA, appFlowyDataFolder); // final initialPath = p.join(userA, appFlowyDataFolder);
final context = await tester.initializeAppFlowy( // final context = await tester.initializeAppFlowy(
pathExtension: initialPath, // pathExtension: initialPath,
); // );
// remove the last extension // // remove the last extension
final rootPath = context.applicationDataDirectory.replaceFirst( // final rootPath = context.applicationDataDirectory.replaceFirst(
initialPath, // initialPath,
'', // '',
); // );
await tester.tapGoButton(); // await tester.tapGoButton();
await tester.expectToSeeHomePageWithGetStartedPage(); // await tester.expectToSeeHomePageWithGetStartedPage();
// switch to user B // // switch to user B
{ // {
// set user name for userA // // set user name for userA
await tester.openSettings(); // await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user); // await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userA); // await tester.enterUserName(userA);
await tester.openSettingsPage(SettingsPage.files); // await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle(); // await tester.pumpAndSettle();
// mock the file_picker result // // mock the file_picker result
await mockGetDirectoryPath( // await mockGetDirectoryPath(
p.join(rootPath, userB), // p.join(rootPath, userB),
); // );
await tester.tapCustomLocationButton(); // await tester.tapCustomLocationButton();
await tester.pumpAndSettle(); // await tester.pumpAndSettle();
await tester.expectToSeeHomePageWithGetStartedPage(); // await tester.expectToSeeHomePageWithGetStartedPage();
// set user name for userB // // set user name for userB
await tester.openSettings(); // await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user); // await tester.openSettingsPage(SettingsPage.user);
await tester.enterUserName(userB); // await tester.enterUserName(userB);
} // }
// switch to the userA // // switch to the userA
{ // {
await tester.openSettingsPage(SettingsPage.files); // await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle(); // await tester.pumpAndSettle();
// mock the file_picker result // // mock the file_picker result
await mockGetDirectoryPath( // await mockGetDirectoryPath(
p.join(rootPath, userA), // p.join(rootPath, userA),
); // );
await tester.tapCustomLocationButton(); // await tester.tapCustomLocationButton();
await tester.pumpAndSettle(); // await tester.pumpAndSettle();
await tester.expectToSeeHomePageWithGetStartedPage(); // await tester.expectToSeeHomePageWithGetStartedPage();
tester.expectToSeeUserName(userA); // tester.expectToSeeUserName(userA);
} // }
// switch to the userB again // // switch to the userB again
{ // {
await tester.openSettings(); // await tester.openSettings();
await tester.openSettingsPage(SettingsPage.files); // await tester.openSettingsPage(SettingsPage.files);
await tester.pumpAndSettle(); // await tester.pumpAndSettle();
// mock the file_picker result // // mock the file_picker result
await mockGetDirectoryPath( // await mockGetDirectoryPath(
p.join(rootPath, userB), // p.join(rootPath, userB),
); // );
await tester.tapCustomLocationButton(); // await tester.tapCustomLocationButton();
await tester.pumpAndSettle(); // await tester.pumpAndSettle();
await tester.expectToSeeHomePageWithGetStartedPage(); // await tester.expectToSeeHomePageWithGetStartedPage();
tester.expectToSeeUserName(userB); // tester.expectToSeeUserName(userB);
} // }
}); // });
testWidgets('reset to default location', (tester) async { testWidgets('reset to default location', (tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();

View File

@ -113,11 +113,10 @@ class _MobileWorkspace extends StatelessWidget {
} }
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_showSwitchWorkspacesBottomSheet( context.read<UserWorkspaceBloc>().add(
context, const UserWorkspaceEvent.fetchWorkspaces(),
currentWorkspace, );
workspaces, _showSwitchWorkspacesBottomSheet(context);
);
}, },
child: Row( child: Row(
children: [ children: [
@ -166,8 +165,6 @@ class _MobileWorkspace extends StatelessWidget {
void _showSwitchWorkspacesBottomSheet( void _showSwitchWorkspacesBottomSheet(
BuildContext context, BuildContext context,
UserWorkspacePB currentWorkspace,
List<UserWorkspacePB> workspaces,
) { ) {
showMobileBottomSheet( showMobileBottomSheet(
context, context,
@ -176,23 +173,35 @@ class _MobileWorkspace extends StatelessWidget {
showDragHandle: true, showDragHandle: true,
title: LocaleKeys.workspace_menuTitle.tr(), title: LocaleKeys.workspace_menuTitle.tr(),
builder: (_) { builder: (_) {
return MobileWorkspaceMenu( return BlocProvider.value(
userProfile: userProfile, value: context.read<UserWorkspaceBloc>(),
currentWorkspace: currentWorkspace, child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
workspaces: workspaces, builder: (context, state) {
onWorkspaceSelected: (workspace) { final currentWorkspace = state.currentWorkspace;
context.pop(); final workspaces = state.workspaces;
if (currentWorkspace == null || workspaces.isEmpty) {
return const SizedBox.shrink();
}
return MobileWorkspaceMenu(
userProfile: userProfile,
currentWorkspace: currentWorkspace,
workspaces: workspaces,
onWorkspaceSelected: (workspace) {
context.pop();
if (workspace == currentWorkspace) { if (workspace == currentWorkspace) {
return; return;
} }
context.read<UserWorkspaceBloc>().add( context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.openWorkspace( UserWorkspaceEvent.openWorkspace(
workspace.workspaceId, workspace.workspaceId,
), ),
); );
}, },
);
},
),
); );
}, },
); );

View File

@ -14,6 +14,9 @@ import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart'; import 'package:flowy_infra/notifier.dart';
typedef DidUserWorkspaceUpdateCallback = void Function(
RepeatedUserWorkspacePB workspaces,
);
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>; typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
typedef AuthNotifyValue = FlowyResult<void, FlowyError>; typedef AuthNotifyValue = FlowyResult<void, FlowyError>;
@ -27,14 +30,20 @@ class UserListener {
UserNotificationParser? _userParser; UserNotificationParser? _userParser;
StreamSubscription<SubscribeObject>? _subscription; StreamSubscription<SubscribeObject>? _subscription;
PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier(); PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
DidUserWorkspaceUpdateCallback? didUpdateUserWorkspaces;
void start({ void start({
void Function(UserProfileNotifyValue)? onProfileUpdated, void Function(UserProfileNotifyValue)? onProfileUpdated,
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
}) { }) {
if (onProfileUpdated != null) { if (onProfileUpdated != null) {
_profileNotifier?.addPublishListener(onProfileUpdated); _profileNotifier?.addPublishListener(onProfileUpdated);
} }
if (didUpdateUserWorkspaces != null) {
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
}
_userParser = UserNotificationParser( _userParser = UserNotificationParser(
id: _userProfile.id.toString(), id: _userProfile.id.toString(),
callback: _userNotificationCallback, callback: _userNotificationCallback,
@ -63,6 +72,14 @@ class UserListener {
(error) => _profileNotifier?.value = FlowyResult.failure(error), (error) => _profileNotifier?.value = FlowyResult.failure(error),
); );
break; break;
case user.UserNotification.DidUpdateUserWorkspaces:
result.map(
(r) {
final value = RepeatedUserWorkspacePB.fromBuffer(r);
didUpdateUserWorkspaces?.call(value);
},
);
break;
default: default:
break; break;
} }
@ -108,6 +125,7 @@ class UserWorkspaceListener {
_settingChangedNotifier?.value = FlowyResult.failure(error), _settingChangedNotifier?.value = FlowyResult.failure(error),
); );
break; break;
default: default:
break; break;
} }

View File

@ -3,6 +3,7 @@ import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.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/user/application/user_listener.dart';
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/code.pbenum.dart';
@ -22,11 +23,18 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
UserWorkspaceBloc({ UserWorkspaceBloc({
required this.userProfile, required this.userProfile,
}) : _userService = UserBackendService(userId: userProfile.id), }) : _userService = UserBackendService(userId: userProfile.id),
_listener = UserListener(userProfile: userProfile),
super(UserWorkspaceState.initial()) { super(UserWorkspaceState.initial()) {
on<UserWorkspaceEvent>( on<UserWorkspaceEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async { initial: () async {
_listener
..didUpdateUserWorkspaces = (workspaces) {
add(UserWorkspaceEvent.updateWorkspaces(workspaces));
}
..start();
final result = await _fetchWorkspaces(); final result = await _fetchWorkspaces();
final isCollabWorkspaceOn = final isCollabWorkspaceOn =
userProfile.authenticator != AuthenticatorPB.Local && userProfile.authenticator != AuthenticatorPB.Local &&
@ -237,13 +245,27 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
), ),
); );
}, },
updateWorkspaces: (workspaces) async {
emit(
state.copyWith(
workspaces: workspaces.items,
),
);
},
); );
}, },
); );
} }
@override
Future<void> close() {
_listener.stop();
return super.close();
}
final UserProfilePB userProfile; final UserProfilePB userProfile;
final UserBackendService _userService; final UserBackendService _userService;
final UserListener _listener;
Future< Future<
( (
@ -270,7 +292,10 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
currentWorkspaceInList ??= workspaces.first; currentWorkspaceInList ??= workspaces.first;
return ( return (
currentWorkspaceInList, currentWorkspaceInList,
workspaces, workspaces
..sort(
(a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp),
),
lastOpenedWorkspaceId != currentWorkspace.id lastOpenedWorkspaceId != currentWorkspace.id
); );
} catch (e) { } catch (e) {
@ -300,6 +325,9 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent {
) = _UpdateWorkspaceIcon; ) = _UpdateWorkspaceIcon;
const factory UserWorkspaceEvent.leaveWorkspace(String workspaceId) = const factory UserWorkspaceEvent.leaveWorkspace(String workspaceId) =
LeaveWorkspace; LeaveWorkspace;
const factory UserWorkspaceEvent.updateWorkspaces(
RepeatedUserWorkspacePB workspaces,
) = UpdateWorkspaces;
} }
enum UserWorkspaceActionType { enum UserWorkspaceActionType {
@ -339,13 +367,16 @@ class UserWorkspaceState with _$UserWorkspaceState {
@override @override
int get hashCode => runtimeType.hashCode; int get hashCode => runtimeType.hashCode;
final DeepCollectionEquality _deepCollectionEquality =
const DeepCollectionEquality();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other is UserWorkspaceState && return other is UserWorkspaceState &&
other.currentWorkspace == currentWorkspace && other.currentWorkspace == currentWorkspace &&
other.workspaces == workspaces && _deepCollectionEquality.equals(other.workspaces, workspaces) &&
identical(other.actionResult, actionResult); identical(other.actionResult, actionResult);
} }
} }

View File

@ -148,6 +148,11 @@ class _SidebarSwitchWorkspaceButtonState
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 10), offset: const Offset(0, 10),
constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600), constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600),
onOpen: () {
context.read<UserWorkspaceBloc>().add(
const UserWorkspaceEvent.fetchWorkspaces(),
);
},
popupBuilder: (_) { popupBuilder: (_) {
return BlocProvider<UserWorkspaceBloc>.value( return BlocProvider<UserWorkspaceBloc>.value(
value: context.read<UserWorkspaceBloc>(), value: context.read<UserWorkspaceBloc>(),

View File

@ -61,6 +61,7 @@ class WorkspacesMenu extends StatelessWidget {
), ),
for (final workspace in workspaces) ...[ for (final workspace in workspaces) ...[
WorkspaceMenuItem( WorkspaceMenuItem(
key: ValueKey(workspace.workspaceId),
workspace: workspace, workspace: workspace,
userProfile: userProfile, userProfile: userProfile,
isSelected: workspace.workspaceId == currentWorkspace.workspaceId, isSelected: workspace.workspaceId == currentWorkspace.workspaceId,

View File

@ -1,8 +1,7 @@
import 'package:appflowy_popover/src/layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:appflowy_popover/src/layout.dart';
import 'mask.dart'; import 'mask.dart';
import 'mutex.dart'; import 'mutex.dart';
@ -79,7 +78,8 @@ class Popover extends StatefulWidget {
/// The direction of the popover /// The direction of the popover
final PopoverDirection direction; final PopoverDirection direction;
final void Function()? onClose; final VoidCallback? onOpen;
final VoidCallback? onClose;
final Future<bool> Function()? canClose; final Future<bool> Function()? canClose;
final bool asBarrier; final bool asBarrier;
@ -109,6 +109,7 @@ class Popover extends StatefulWidget {
this.direction = PopoverDirection.rightWithTopAligned, this.direction = PopoverDirection.rightWithTopAligned,
this.mutex, this.mutex,
this.windowPadding, this.windowPadding,
this.onOpen,
this.onClose, this.onClose,
this.canClose, this.canClose,
this.asBarrier = false, this.asBarrier = false,
@ -228,6 +229,7 @@ class PopoverState extends State<Popover> {
child: _buildClickHandler( child: _buildClickHandler(
widget.child, widget.child,
() { () {
widget.onOpen?.call();
if (widget.triggerActions & PopoverTriggerFlags.click != 0) { if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
showOverlay(); showOverlay();
} }

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
class AppFlowyPopover extends StatelessWidget { class AppFlowyPopover extends StatelessWidget {
final Widget child; final Widget child;
@ -10,7 +9,8 @@ class AppFlowyPopover extends StatelessWidget {
final PopoverDirection direction; final PopoverDirection direction;
final int triggerActions; final int triggerActions;
final BoxConstraints constraints; final BoxConstraints constraints;
final void Function()? onClose; final VoidCallback? onOpen;
final VoidCallback? onClose;
final Future<bool> Function()? canClose; final Future<bool> Function()? canClose;
final PopoverMutex? mutex; final PopoverMutex? mutex;
final Offset? offset; final Offset? offset;
@ -35,6 +35,7 @@ class AppFlowyPopover extends StatelessWidget {
required this.child, required this.child,
required this.popupBuilder, required this.popupBuilder,
this.direction = PopoverDirection.rightWithTopAligned, this.direction = PopoverDirection.rightWithTopAligned,
this.onOpen,
this.onClose, this.onClose,
this.canClose, this.canClose,
this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600), this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600),
@ -54,6 +55,7 @@ class AppFlowyPopover extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Popover( return Popover(
controller: controller, controller: controller,
onOpen: onOpen,
onClose: onClose, onClose: onClose,
canClose: canClose, canClose: canClose,
direction: direction, direction: direction,

View File

@ -332,14 +332,24 @@ pub fn save_user_workspaces(
) -> FlowyResult<()> { ) -> FlowyResult<()> {
let user_workspaces = user_workspaces let user_workspaces = user_workspaces
.iter() .iter()
.flat_map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)).ok()) .map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)))
.collect::<Vec<UserWorkspaceTable>>(); .collect::<Result<Vec<_>, _>>()?;
conn.immediate_transaction(|conn| { conn.immediate_transaction(|conn| {
for user_workspace in user_workspaces { let existing_ids = user_workspace_table::dsl::user_workspace_table
if let Err(err) = diesel::update( .select(user_workspace_table::id)
.load::<String>(conn)?;
let new_ids: Vec<String> = user_workspaces.iter().map(|w| w.id.clone()).collect();
let ids_to_delete: Vec<String> = existing_ids
.into_iter()
.filter(|id| !new_ids.contains(id))
.collect();
// insert or update the user workspaces
for user_workspace in &user_workspaces {
let affected_rows = diesel::update(
user_workspace_table::dsl::user_workspace_table user_workspace_table::dsl::user_workspace_table
.filter(user_workspace_table::id.eq(user_workspace.id.clone())), .filter(user_workspace_table::id.eq(&user_workspace.id)),
) )
.set(( .set((
user_workspace_table::name.eq(&user_workspace.name), user_workspace_table::name.eq(&user_workspace.name),
@ -347,18 +357,24 @@ pub fn save_user_workspaces(
user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id),
user_workspace_table::icon.eq(&user_workspace.icon), user_workspace_table::icon.eq(&user_workspace.icon),
)) ))
.execute(conn) .execute(conn)?;
.and_then(|rows| {
if rows == 0 { if affected_rows == 0 {
let _ = diesel::insert_into(user_workspace_table::table) diesel::insert_into(user_workspace_table::table)
.values(user_workspace) .values(user_workspace)
.execute(conn)?; .execute(conn)?;
}
Ok(())
}) {
tracing::error!("Error saving user workspace: {:?}", err);
} }
} }
// delete the user workspaces that are not in the new list
if !ids_to_delete.is_empty() {
diesel::delete(
user_workspace_table::dsl::user_workspace_table
.filter(user_workspace_table::id.eq_any(ids_to_delete)),
)
.execute(conn)?;
}
Ok::<(), FlowyError>(()) Ok::<(), FlowyError>(())
}) })
} }