mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
1816b15b55
commit
096a01ed44
@ -5,9 +5,7 @@ import 'package:appflowy/startup/tasks/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
@ -18,80 +16,80 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const userA = 'UserA';
|
||||
const userB = 'UserB';
|
||||
// testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
// const userA = 'UserA';
|
||||
// const userB = 'UserB';
|
||||
|
||||
final initialPath = p.join(userA, appFlowyDataFolder);
|
||||
final context = await tester.initializeAppFlowy(
|
||||
pathExtension: initialPath,
|
||||
);
|
||||
// remove the last extension
|
||||
final rootPath = context.applicationDataDirectory.replaceFirst(
|
||||
initialPath,
|
||||
'',
|
||||
);
|
||||
// final initialPath = p.join(userA, appFlowyDataFolder);
|
||||
// final context = await tester.initializeAppFlowy(
|
||||
// pathExtension: initialPath,
|
||||
// );
|
||||
// // remove the last extension
|
||||
// final rootPath = context.applicationDataDirectory.replaceFirst(
|
||||
// initialPath,
|
||||
// '',
|
||||
// );
|
||||
|
||||
await tester.tapGoButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// await tester.tapGoButton();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// switch to user B
|
||||
{
|
||||
// set user name for userA
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userA);
|
||||
// // switch to user B
|
||||
// {
|
||||
// // set user name for userA
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.user);
|
||||
// await tester.enterUserName(userA);
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
// await tester.openSettingsPage(SettingsPage.files);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userB),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// // mock the file_picker result
|
||||
// await mockGetDirectoryPath(
|
||||
// p.join(rootPath, userB),
|
||||
// );
|
||||
// await tester.tapCustomLocationButton();
|
||||
// await tester.pumpAndSettle();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// set user name for userB
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userB);
|
||||
}
|
||||
// // set user name for userB
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.user);
|
||||
// await tester.enterUserName(userB);
|
||||
// }
|
||||
|
||||
// switch to the userA
|
||||
{
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
// // switch to the userA
|
||||
// {
|
||||
// await tester.openSettingsPage(SettingsPage.files);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userA),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
// // mock the file_picker result
|
||||
// await mockGetDirectoryPath(
|
||||
// p.join(rootPath, userA),
|
||||
// );
|
||||
// await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
tester.expectToSeeUserName(userA);
|
||||
}
|
||||
// await tester.pumpAndSettle();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// tester.expectToSeeUserName(userA);
|
||||
// }
|
||||
|
||||
// switch to the userB again
|
||||
{
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
// // switch to the userB again
|
||||
// {
|
||||
// await tester.openSettings();
|
||||
// await tester.openSettingsPage(SettingsPage.files);
|
||||
// await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(
|
||||
p.join(rootPath, userB),
|
||||
);
|
||||
await tester.tapCustomLocationButton();
|
||||
// // mock the file_picker result
|
||||
// await mockGetDirectoryPath(
|
||||
// p.join(rootPath, userB),
|
||||
// );
|
||||
// await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
tester.expectToSeeUserName(userB);
|
||||
}
|
||||
});
|
||||
// await tester.pumpAndSettle();
|
||||
// await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
// tester.expectToSeeUserName(userB);
|
||||
// }
|
||||
// });
|
||||
|
||||
testWidgets('reset to default location', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
@ -113,11 +113,10 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showSwitchWorkspacesBottomSheet(
|
||||
context,
|
||||
currentWorkspace,
|
||||
workspaces,
|
||||
);
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
const UserWorkspaceEvent.fetchWorkspaces(),
|
||||
);
|
||||
_showSwitchWorkspacesBottomSheet(context);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
@ -166,8 +165,6 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
|
||||
void _showSwitchWorkspacesBottomSheet(
|
||||
BuildContext context,
|
||||
UserWorkspacePB currentWorkspace,
|
||||
List<UserWorkspacePB> workspaces,
|
||||
) {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
@ -176,23 +173,35 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
showDragHandle: true,
|
||||
title: LocaleKeys.workspace_menuTitle.tr(),
|
||||
builder: (_) {
|
||||
return MobileWorkspaceMenu(
|
||||
userProfile: userProfile,
|
||||
currentWorkspace: currentWorkspace,
|
||||
workspaces: workspaces,
|
||||
onWorkspaceSelected: (workspace) {
|
||||
context.pop();
|
||||
return BlocProvider.value(
|
||||
value: context.read<UserWorkspaceBloc>(),
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
builder: (context, state) {
|
||||
final currentWorkspace = state.currentWorkspace;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
if (workspace == currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.openWorkspace(
|
||||
workspace.workspaceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -14,6 +14,9 @@ import 'package:appflowy_backend/rust_stream.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef DidUserWorkspaceUpdateCallback = void Function(
|
||||
RepeatedUserWorkspacePB workspaces,
|
||||
);
|
||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||
typedef AuthNotifyValue = FlowyResult<void, FlowyError>;
|
||||
|
||||
@ -27,14 +30,20 @@ class UserListener {
|
||||
UserNotificationParser? _userParser;
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
|
||||
DidUserWorkspaceUpdateCallback? didUpdateUserWorkspaces;
|
||||
|
||||
void start({
|
||||
void Function(UserProfileNotifyValue)? onProfileUpdated,
|
||||
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
|
||||
}) {
|
||||
if (onProfileUpdated != null) {
|
||||
_profileNotifier?.addPublishListener(onProfileUpdated);
|
||||
}
|
||||
|
||||
if (didUpdateUserWorkspaces != null) {
|
||||
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
|
||||
}
|
||||
|
||||
_userParser = UserNotificationParser(
|
||||
id: _userProfile.id.toString(),
|
||||
callback: _userNotificationCallback,
|
||||
@ -63,6 +72,14 @@ class UserListener {
|
||||
(error) => _profileNotifier?.value = FlowyResult.failure(error),
|
||||
);
|
||||
break;
|
||||
case user.UserNotification.DidUpdateUserWorkspaces:
|
||||
result.map(
|
||||
(r) {
|
||||
final value = RepeatedUserWorkspacePB.fromBuffer(r);
|
||||
didUpdateUserWorkspaces?.call(value);
|
||||
},
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -108,6 +125,7 @@ class UserWorkspaceListener {
|
||||
_settingChangedNotifier?.value = FlowyResult.failure(error),
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'package:appflowy/core/config/kv_keys.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/user_listener.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
|
||||
@ -22,11 +23,18 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
UserWorkspaceBloc({
|
||||
required this.userProfile,
|
||||
}) : _userService = UserBackendService(userId: userProfile.id),
|
||||
_listener = UserListener(userProfile: userProfile),
|
||||
super(UserWorkspaceState.initial()) {
|
||||
on<UserWorkspaceEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_listener
|
||||
..didUpdateUserWorkspaces = (workspaces) {
|
||||
add(UserWorkspaceEvent.updateWorkspaces(workspaces));
|
||||
}
|
||||
..start();
|
||||
|
||||
final result = await _fetchWorkspaces();
|
||||
final isCollabWorkspaceOn =
|
||||
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 UserBackendService _userService;
|
||||
final UserListener _listener;
|
||||
|
||||
Future<
|
||||
(
|
||||
@ -270,7 +292,10 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
currentWorkspaceInList ??= workspaces.first;
|
||||
return (
|
||||
currentWorkspaceInList,
|
||||
workspaces,
|
||||
workspaces
|
||||
..sort(
|
||||
(a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp),
|
||||
),
|
||||
lastOpenedWorkspaceId != currentWorkspace.id
|
||||
);
|
||||
} catch (e) {
|
||||
@ -300,6 +325,9 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent {
|
||||
) = _UpdateWorkspaceIcon;
|
||||
const factory UserWorkspaceEvent.leaveWorkspace(String workspaceId) =
|
||||
LeaveWorkspace;
|
||||
const factory UserWorkspaceEvent.updateWorkspaces(
|
||||
RepeatedUserWorkspacePB workspaces,
|
||||
) = UpdateWorkspaces;
|
||||
}
|
||||
|
||||
enum UserWorkspaceActionType {
|
||||
@ -339,13 +367,16 @@ class UserWorkspaceState with _$UserWorkspaceState {
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
final DeepCollectionEquality _deepCollectionEquality =
|
||||
const DeepCollectionEquality();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UserWorkspaceState &&
|
||||
other.currentWorkspace == currentWorkspace &&
|
||||
other.workspaces == workspaces &&
|
||||
_deepCollectionEquality.equals(other.workspaces, workspaces) &&
|
||||
identical(other.actionResult, actionResult);
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +148,11 @@ class _SidebarSwitchWorkspaceButtonState
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 10),
|
||||
constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600),
|
||||
onOpen: () {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
const UserWorkspaceEvent.fetchWorkspaces(),
|
||||
);
|
||||
},
|
||||
popupBuilder: (_) {
|
||||
return BlocProvider<UserWorkspaceBloc>.value(
|
||||
value: context.read<UserWorkspaceBloc>(),
|
||||
|
@ -61,6 +61,7 @@ class WorkspacesMenu extends StatelessWidget {
|
||||
),
|
||||
for (final workspace in workspaces) ...[
|
||||
WorkspaceMenuItem(
|
||||
key: ValueKey(workspace.workspaceId),
|
||||
workspace: workspace,
|
||||
userProfile: userProfile,
|
||||
isSelected: workspace.workspaceId == currentWorkspace.workspaceId,
|
||||
|
@ -1,8 +1,7 @@
|
||||
import 'package:appflowy_popover/src/layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy_popover/src/layout.dart';
|
||||
|
||||
import 'mask.dart';
|
||||
import 'mutex.dart';
|
||||
|
||||
@ -79,7 +78,8 @@ class Popover extends StatefulWidget {
|
||||
/// The direction of the popover
|
||||
final PopoverDirection direction;
|
||||
|
||||
final void Function()? onClose;
|
||||
final VoidCallback? onOpen;
|
||||
final VoidCallback? onClose;
|
||||
final Future<bool> Function()? canClose;
|
||||
|
||||
final bool asBarrier;
|
||||
@ -109,6 +109,7 @@ class Popover extends StatefulWidget {
|
||||
this.direction = PopoverDirection.rightWithTopAligned,
|
||||
this.mutex,
|
||||
this.windowPadding,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
this.canClose,
|
||||
this.asBarrier = false,
|
||||
@ -228,6 +229,7 @@ class PopoverState extends State<Popover> {
|
||||
child: _buildClickHandler(
|
||||
widget.child,
|
||||
() {
|
||||
widget.onOpen?.call();
|
||||
if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
|
||||
showOverlay();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppFlowyPopover extends StatelessWidget {
|
||||
final Widget child;
|
||||
@ -10,7 +9,8 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
final PopoverDirection direction;
|
||||
final int triggerActions;
|
||||
final BoxConstraints constraints;
|
||||
final void Function()? onClose;
|
||||
final VoidCallback? onOpen;
|
||||
final VoidCallback? onClose;
|
||||
final Future<bool> Function()? canClose;
|
||||
final PopoverMutex? mutex;
|
||||
final Offset? offset;
|
||||
@ -35,6 +35,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
required this.child,
|
||||
required this.popupBuilder,
|
||||
this.direction = PopoverDirection.rightWithTopAligned,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
this.canClose,
|
||||
this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600),
|
||||
@ -54,6 +55,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Popover(
|
||||
controller: controller,
|
||||
onOpen: onOpen,
|
||||
onClose: onClose,
|
||||
canClose: canClose,
|
||||
direction: direction,
|
||||
|
@ -332,14 +332,24 @@ pub fn save_user_workspaces(
|
||||
) -> FlowyResult<()> {
|
||||
let user_workspaces = user_workspaces
|
||||
.iter()
|
||||
.flat_map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)).ok())
|
||||
.collect::<Vec<UserWorkspaceTable>>();
|
||||
.map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
conn.immediate_transaction(|conn| {
|
||||
for user_workspace in user_workspaces {
|
||||
if let Err(err) = diesel::update(
|
||||
let existing_ids = user_workspace_table::dsl::user_workspace_table
|
||||
.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
|
||||
.filter(user_workspace_table::id.eq(user_workspace.id.clone())),
|
||||
.filter(user_workspace_table::id.eq(&user_workspace.id)),
|
||||
)
|
||||
.set((
|
||||
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::icon.eq(&user_workspace.icon),
|
||||
))
|
||||
.execute(conn)
|
||||
.and_then(|rows| {
|
||||
if rows == 0 {
|
||||
let _ = diesel::insert_into(user_workspace_table::table)
|
||||
.values(user_workspace)
|
||||
.execute(conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}) {
|
||||
tracing::error!("Error saving user workspace: {:?}", err);
|
||||
.execute(conn)?;
|
||||
|
||||
if affected_rows == 0 {
|
||||
diesel::insert_into(user_workspace_table::table)
|
||||
.values(user_workspace)
|
||||
.execute(conn)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 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>(())
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user