Skip to content

Commit 8826e47

Browse files
fix(flutter_desktop): workspace menu ui issues (#7091)
* fix(flutter_desktop): remove log out and workspace option popovers conflict * test: add integration test * fix(flutter_desktop): workspace list scrollbar overlaps with list * chore(flutter_desktop): fix padding around import from notion button * chore(flutter_desktop): adjust popover conflict rules for workspace * test: add integration tests * chore(flutter_desktop): make the popoovers as barriers * fix: regression from making the workspace item menu as barrier * chore: update frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart Co-authored-by: Lucas <[email protected]> --------- Co-authored-by: Lucas <[email protected]>
1 parent 8e4fe3d commit 8826e47

File tree

4 files changed

+154
-68
lines changed

4 files changed

+154
-68
lines changed

frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:appflowy/shared/feature_flags.dart';
55
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
66
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
77
import 'package:easy_localization/easy_localization.dart';
8+
import 'package:flutter/services.dart';
89
import 'package:flutter_test/flutter_test.dart';
910
import 'package:integration_test/integration_test.dart';
1011

@@ -102,8 +103,7 @@ void main() {
102103
expect(memberCount, findsNWidgets(2));
103104
});
104105

105-
testWidgets('only display one menu item in the workspace menu',
106-
(tester) async {
106+
testWidgets('workspace menu popover behavior test', (tester) async {
107107
// only run the test when the feature flag is on
108108
if (!FeatureFlag.collaborativeWorkspace.isOn) {
109109
return;
@@ -128,6 +128,8 @@ void main() {
128128
final workspaceItem = find.byWidgetPredicate(
129129
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
130130
);
131+
132+
// the workspace menu shouldn't conflict with logout
131133
await tester.hoverOnWidget(
132134
workspaceItem,
133135
onHover: () async {
@@ -136,15 +138,73 @@ void main() {
136138
);
137139
expect(moreButton, findsOneWidget);
138140
await tester.tapButton(moreButton);
141+
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
142+
143+
final logoutButton = find.byType(WorkspaceMoreButton);
144+
await tester.tapButton(logoutButton);
145+
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
146+
expect(moreButton, findsNothing);
147+
148+
await tester.tapButton(moreButton);
149+
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
150+
expect(moreButton, findsOneWidget);
151+
},
152+
);
153+
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
154+
await tester.pumpAndSettle();
155+
156+
// clicking on the more action button for the same workspace shouldn't do
157+
// anything
158+
await tester.openCollaborativeWorkspaceMenu();
159+
await tester.hoverOnWidget(
160+
workspaceItem,
161+
onHover: () async {
162+
final moreButton = find.byWidgetPredicate(
163+
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
164+
);
165+
expect(moreButton, findsOneWidget);
166+
await tester.tapButton(moreButton);
167+
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
139168

140169
// click it again
141170
await tester.tapButton(moreButton);
142171

143172
// nothing should happen
144-
expect(
145-
find.text(LocaleKeys.button_rename.tr()),
146-
findsOneWidget,
147-
);
173+
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
174+
},
175+
);
176+
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
177+
await tester.pumpAndSettle();
178+
179+
// clicking on the more button of another workspace should close the menu
180+
// for this one
181+
await tester.openCollaborativeWorkspaceMenu();
182+
final moreButton = find.byWidgetPredicate(
183+
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
184+
);
185+
await tester.hoverOnWidget(
186+
workspaceItem,
187+
onHover: () async {
188+
expect(moreButton, findsOneWidget);
189+
await tester.tapButton(moreButton);
190+
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
191+
},
192+
);
193+
194+
final otherWorspaceItem = find.byWidgetPredicate(
195+
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
196+
);
197+
final otherMoreButton = find.byWidgetPredicate(
198+
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
199+
);
200+
await tester.hoverOnWidget(
201+
otherWorspaceItem,
202+
onHover: () async {
203+
expect(otherMoreButton, findsOneWidget);
204+
await tester.tapButton(otherMoreButton);
205+
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
206+
207+
expect(moreButton, findsNothing);
148208
},
149209
);
150210
});

frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,23 @@ enum WorkspaceMoreAction {
1818
divider,
1919
}
2020

21-
class WorkspaceMoreActionList extends StatelessWidget {
21+
class WorkspaceMoreActionList extends StatefulWidget {
2222
const WorkspaceMoreActionList({
2323
super.key,
2424
required this.workspace,
25-
required this.isShowingMoreActions,
25+
required this.popoverMutex,
2626
});
2727

2828
final UserWorkspacePB workspace;
29-
final ValueNotifier<bool> isShowingMoreActions;
29+
final PopoverMutex popoverMutex;
30+
31+
@override
32+
State<WorkspaceMoreActionList> createState() =>
33+
_WorkspaceMoreActionListState();
34+
}
35+
36+
class _WorkspaceMoreActionListState extends State<WorkspaceMoreActionList> {
37+
bool isPopoverOpen = false;
3038

3139
@override
3240
Widget build(BuildContext context) {
@@ -45,16 +53,22 @@ class WorkspaceMoreActionList extends StatelessWidget {
4553
return PopoverActionList<_WorkspaceMoreActionWrapper>(
4654
direction: PopoverDirection.bottomWithLeftAligned,
4755
actions: actions
48-
.map((e) => _WorkspaceMoreActionWrapper(e, workspace))
56+
.map(
57+
(action) => _WorkspaceMoreActionWrapper(
58+
action,
59+
widget.workspace,
60+
() => PopoverContainer.of(context).closeAll(),
61+
),
62+
)
4963
.toList(),
64+
mutex: widget.popoverMutex,
5065
constraints: const BoxConstraints(minWidth: 220),
5166
animationDuration: Durations.short3,
5267
slideDistance: 2,
5368
beginScaleFactor: 1.0,
5469
beginOpacity: 0.8,
55-
onClosed: () {
56-
isShowingMoreActions.value = false;
57-
},
70+
onClosed: () => isPopoverOpen = false,
71+
asBarrier: true,
5872
buildChild: (controller) {
5973
return SizedBox.square(
6074
dimension: 24.0,
@@ -64,11 +78,10 @@ class WorkspaceMoreActionList extends StatelessWidget {
6478
FlowySvgs.workspace_three_dots_s,
6579
),
6680
onTap: () {
67-
if (!isShowingMoreActions.value) {
81+
if (!isPopoverOpen) {
6882
controller.show();
83+
isPopoverOpen = true;
6984
}
70-
71-
isShowingMoreActions.value = true;
7285
},
7386
),
7487
);
@@ -79,10 +92,15 @@ class WorkspaceMoreActionList extends StatelessWidget {
7992
}
8093

8194
class _WorkspaceMoreActionWrapper extends CustomActionCell {
82-
_WorkspaceMoreActionWrapper(this.inner, this.workspace);
95+
_WorkspaceMoreActionWrapper(
96+
this.inner,
97+
this.workspace,
98+
this.closeWorkspaceMenu,
99+
);
83100

84101
final WorkspaceMoreAction inner;
85102
final UserWorkspacePB workspace;
103+
final VoidCallback closeWorkspaceMenu;
86104

87105
@override
88106
Widget buildWithContext(
@@ -117,6 +135,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
117135
margin: const EdgeInsets.all(6),
118136
onTap: () async {
119137
PopoverContainer.of(context).closeAll();
138+
closeWorkspaceMenu();
120139

121140
final workspaceBloc = context.read<UserWorkspaceBloc>();
122141
switch (inner) {

frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,7 @@ class WorkspacesMenu extends StatefulWidget {
4343
}
4444

4545
class _WorkspacesMenuState extends State<WorkspacesMenu> {
46-
final ValueNotifier<bool> isShowingMoreActions = ValueNotifier(false);
47-
48-
@override
49-
void dispose() {
50-
isShowingMoreActions.dispose();
51-
super.dispose();
52-
}
46+
final popoverMutex = PopoverMutex();
5347

5448
@override
5549
Widget build(BuildContext context) {
@@ -59,7 +53,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
5953
children: [
6054
// user email
6155
Padding(
62-
padding: const EdgeInsets.symmetric(horizontal: 4.0),
56+
padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0),
6357
child: Row(
6458
children: [
6559
Expanded(
@@ -71,18 +65,21 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
7165
),
7266
),
7367
const HSpace(4.0),
74-
const _WorkspaceMoreButton(),
68+
WorkspaceMoreButton(
69+
popoverMutex: popoverMutex,
70+
),
7571
const HSpace(8.0),
7672
],
7773
),
7874
),
7975
const Padding(
80-
padding: EdgeInsets.symmetric(vertical: 8.0),
76+
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0),
8177
child: Divider(height: 1.0),
8278
),
8379
// workspace list
8480
Flexible(
8581
child: SingleChildScrollView(
82+
padding: const EdgeInsets.symmetric(horizontal: 6.0),
8683
child: Column(
8784
mainAxisSize: MainAxisSize.min,
8885
children: [
@@ -93,7 +90,7 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
9390
userProfile: widget.userProfile,
9491
isSelected: workspace.workspaceId ==
9592
widget.currentWorkspace.workspaceId,
96-
isShowingMoreActions: isShowingMoreActions,
93+
popoverMutex: popoverMutex,
9794
),
9895
const VSpace(6.0),
9996
],
@@ -102,13 +99,19 @@ class _WorkspacesMenuState extends State<WorkspacesMenu> {
10299
),
103100
),
104101
// add new workspace
105-
const _CreateWorkspaceButton(),
106-
const VSpace(6.0),
102+
const Padding(
103+
padding: EdgeInsets.symmetric(horizontal: 6.0),
104+
child: _CreateWorkspaceButton(),
105+
),
107106

108107
if (UniversalPlatform.isDesktop) ...[
109-
const _ImportNotionButton(),
110-
const VSpace(6.0),
108+
const Padding(
109+
padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0),
110+
child: _ImportNotionButton(),
111+
),
111112
],
113+
114+
const VSpace(6.0),
112115
],
113116
);
114117
}
@@ -132,13 +135,13 @@ class WorkspaceMenuItem extends StatefulWidget {
132135
required this.workspace,
133136
required this.userProfile,
134137
required this.isSelected,
135-
required this.isShowingMoreActions,
138+
required this.popoverMutex,
136139
});
137140

138141
final UserProfilePB userProfile;
139142
final UserWorkspacePB workspace;
140143
final bool isSelected;
141-
final ValueNotifier<bool> isShowingMoreActions;
144+
final PopoverMutex popoverMutex;
142145

143146
@override
144147
State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();
@@ -230,7 +233,7 @@ class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {
230233
},
231234
child: WorkspaceMoreActionList(
232235
workspace: widget.workspace,
233-
isShowingMoreActions: widget.isShowingMoreActions,
236+
popoverMutex: widget.popoverMutex,
234237
),
235238
),
236239
const HSpace(8.0),
@@ -394,40 +397,35 @@ class _ImportNotionButton extends StatelessWidget {
394397
Widget build(BuildContext context) {
395398
return SizedBox(
396399
height: 40,
397-
child: Stack(
398-
alignment: Alignment.centerRight,
399-
children: [
400-
FlowyButton(
401-
key: importNotionButtonKey,
402-
onTap: () {
403-
_showImportNotinoDialog(context);
404-
},
405-
margin: const EdgeInsets.symmetric(horizontal: 4.0),
406-
text: Row(
407-
children: [
408-
_buildLeftIcon(context),
409-
const HSpace(8.0),
410-
FlowyText.regular(
411-
LocaleKeys.workspace_importFromNotion.tr(),
412-
),
413-
],
400+
child: FlowyButton(
401+
key: importNotionButtonKey,
402+
onTap: () {
403+
_showImportNotinoDialog(context);
404+
},
405+
margin: const EdgeInsets.symmetric(horizontal: 4.0),
406+
text: Row(
407+
children: [
408+
_buildLeftIcon(context),
409+
const HSpace(8.0),
410+
FlowyText.regular(
411+
LocaleKeys.workspace_importFromNotion.tr(),
414412
),
415-
),
416-
FlowyTooltip(
417-
message: LocaleKeys.workspace_learnMore.tr(),
418-
preferBelow: true,
419-
child: FlowyIconButton(
420-
icon: const FlowySvg(
421-
FlowySvgs.information_s,
422-
),
423-
onPressed: () {
424-
afLaunchUrlString(
425-
'https://docs.appflowy.io/docs/guides/import-from-notion',
426-
);
427-
},
413+
],
414+
),
415+
rightIcon: FlowyTooltip(
416+
message: LocaleKeys.workspace_learnMore.tr(),
417+
preferBelow: true,
418+
child: FlowyIconButton(
419+
icon: const FlowySvg(
420+
FlowySvgs.information_s,
428421
),
422+
onPressed: () {
423+
afLaunchUrlString(
424+
'https://docs.appflowy.io/docs/guides/import-from-notion',
425+
);
426+
},
429427
),
430-
],
428+
),
431429
),
432430
);
433431
}
@@ -478,14 +476,22 @@ class _ImportNotionButton extends StatelessWidget {
478476
}
479477
}
480478

481-
class _WorkspaceMoreButton extends StatelessWidget {
482-
const _WorkspaceMoreButton();
479+
@visibleForTesting
480+
class WorkspaceMoreButton extends StatelessWidget {
481+
const WorkspaceMoreButton({
482+
super.key,
483+
required this.popoverMutex,
484+
});
485+
486+
final PopoverMutex popoverMutex;
483487

484488
@override
485489
Widget build(BuildContext context) {
486490
return AppFlowyPopover(
487491
direction: PopoverDirection.bottomWithLeftAligned,
488492
offset: const Offset(0, 6),
493+
mutex: popoverMutex,
494+
asBarrier: true,
489495
popupBuilder: (_) => FlowyButton(
490496
margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),
491497
leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),

0 commit comments

Comments
 (0)