feat: disable moving page into the database (#3107)

This commit is contained in:
Lucas.Xu 2023-08-03 14:06:02 +07:00 committed by GitHub
parent 135d8a811e
commit fb9bc359b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 28 deletions

View File

@ -2,7 +2,9 @@ import 'package:appflowy/plugins/database_view/board/presentation/board_page.dar
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
@ -138,5 +140,70 @@ void main() {
.id,
);
});
testWidgets('unable to move a document into a database', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
const document = 'document';
await tester.createNewPageWithName(
name: document,
openAfterCreated: false,
);
tester.expectToSeePageName(document, layout: ViewLayoutPB.Document);
const grid = 'grid';
await tester.createNewPageWithName(
name: grid,
layout: ViewLayoutPB.Grid,
openAfterCreated: false,
);
tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid);
// move the document to the grid page
await tester.movePageToOtherPage(
name: document,
parentName: grid,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Grid,
);
// it should not be moved
final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.view
.childViews;
expect(
childViews[0].name,
document,
);
expect(
childViews[1].name,
grid,
);
});
testWidgets('unable to create a new database inside the existing one',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
const grid = 'grid';
await tester.createNewPageWithName(
name: grid,
layout: ViewLayoutPB.Grid,
openAfterCreated: true,
);
tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid);
await tester.hoverOnPageName(
grid,
layout: ViewLayoutPB.Grid,
onHover: () async {
expect(find.byType(ViewAddButton), findsNothing);
expect(find.byType(ViewMoreActionButton), findsOneWidget);
},
);
});
});
}

View File

@ -351,7 +351,7 @@ extension CommonOperations on WidgetTester {
await hoverOnPageName(
name,
layout: layout,
useLast: false,
useLast: true,
onHover: () async {
await tapFavoritePageButton();
await pumpAndSettle();
@ -366,7 +366,7 @@ extension CommonOperations on WidgetTester {
await hoverOnPageName(
name,
layout: layout,
useLast: false,
useLast: true,
onHover: () async {
await tapUnfavoritePageButton();
await pumpAndSettle();
@ -397,7 +397,7 @@ extension CommonOperations on WidgetTester {
break;
default:
}
await gesture.moveTo(offset);
await gesture.moveTo(offset, timeStamp: const Duration(milliseconds: 400));
await gesture.up();
await pumpAndSettle();
}

View File

@ -42,7 +42,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
final result = await _workspaceService.createApp(
name: event.name,
desc: event.desc,
index: 0, // default to the first index
index: event.index,
);
result.fold(
(app) => emit(state.copyWith(plugin: app.plugin())),
@ -111,7 +111,8 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
class MenuEvent with _$MenuEvent {
const factory MenuEvent.initial() = _Initial;
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp;
const factory MenuEvent.createApp(String name, {String? desc, int? index}) =
_CreateApp;
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;
const factory MenuEvent.didReceiveApps(
Either<List<ViewPB>, FlowyError> appsOrFail,

View File

@ -125,4 +125,17 @@ extension ViewLayoutExtension on ViewLayoutPB {
throw Exception('Unknown layout type');
}
}
bool get isDatabaseView {
switch (this) {
case ViewLayoutPB.Grid:
case ViewLayoutPB.Board:
case ViewLayoutPB.Calendar:
return true;
case ViewLayoutPB.Document:
return false;
default:
throw Exception('Unknown layout type');
}
}
}

View File

@ -49,6 +49,7 @@ class PersonalFolder extends StatelessWidget {
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
leftPadding: 16,
onSelected: (view) {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
@ -114,6 +115,7 @@ class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
context.read<MenuBloc>().add(
MenuEvent.createApp(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
),
);
widget.onAdded();

View File

@ -48,7 +48,12 @@ class SidebarNewPageButton extends StatelessWidget {
value: '',
confirm: (value) {
if (value.isNotEmpty) {
context.read<MenuBloc>().add(MenuEvent.createApp(value, desc: ''));
context.read<MenuBloc>().add(
MenuEvent.createApp(
value,
desc: '',
),
);
}
},
).show(context);

View File

@ -1,4 +1,5 @@
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@ -70,16 +71,17 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
data: widget.view,
onWillAccept: (data) => true,
onMove: (data) {
if (!_shouldAccept(data.data)) {
return;
}
final renderBox = context.findRenderObject() as RenderBox;
final offset = renderBox.globalToLocal(data.offset);
final position = _computeHoverPosition(offset, renderBox.size);
if (!_shouldAccept(data.data, position)) {
return;
}
setState(() {
position = _computeHoverPosition(offset, renderBox.size);
Log.debug(
'offset: $offset, position: $position, size: ${renderBox.size}',
);
this.position = position;
});
},
onLeave: (_) => setState(
@ -102,6 +104,12 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
}
void _move(ViewPB from, ViewPB to) {
if (position == DraggableHoverPosition.center &&
to.layout != ViewLayoutPB.Document) {
// not support moving into a database
return;
}
switch (position) {
case DraggableHoverPosition.top:
context.read<ViewBloc>().add(
@ -136,7 +144,7 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
}
DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) {
final threshold = size.height / 4.0;
final threshold = size.height / 3.0;
if (widget.isFirstChild && offset.dy < -5.0) {
return DraggableHoverPosition.top;
}
@ -146,7 +154,13 @@ class _DraggableViewItemState extends State<DraggableViewItem> {
return DraggableHoverPosition.center;
}
bool _shouldAccept(ViewPB data) {
bool _shouldAccept(ViewPB data, DraggableHoverPosition position) {
// could not move the view to a database
if (widget.view.layout.isDatabaseView &&
position == DraggableHoverPosition.center) {
return false;
}
// ignore moving the view to itself
if (data.id == widget.view.id) {
return false;

View File

@ -23,6 +23,7 @@ class ViewItem extends StatelessWidget {
const ViewItem({
super.key,
required this.view,
this.parentView,
required this.categoryType,
required this.level,
this.leftPadding = 10,
@ -32,6 +33,7 @@ class ViewItem extends StatelessWidget {
});
final ViewPB view;
final ViewPB? parentView;
final FolderCategoryType categoryType;
@ -60,6 +62,7 @@ class ViewItem extends StatelessWidget {
builder: (context, state) {
return InnerViewItem(
view: state.view,
parentView: parentView,
childViews: state.childViews,
categoryType: categoryType,
level: level,
@ -80,18 +83,20 @@ class InnerViewItem extends StatelessWidget {
const InnerViewItem({
super.key,
required this.view,
required this.parentView,
required this.childViews,
required this.categoryType,
this.isDraggable = true,
this.isExpanded = true,
required this.level,
this.leftPadding = 10,
required this.leftPadding,
required this.showActions,
required this.onSelected,
this.isFirstChild = false,
});
final ViewPB view;
final ViewPB? parentView;
final List<ViewPB> childViews;
final FolderCategoryType categoryType;
@ -109,10 +114,13 @@ class InnerViewItem extends StatelessWidget {
Widget build(BuildContext context) {
Widget child = SingleInnerViewItem(
view: view,
parentView: parentView,
level: level,
showActions: showActions,
onSelected: onSelected,
isExpanded: isExpanded,
isDraggable: isDraggable,
leftPadding: leftPadding,
);
// if the view is expanded and has child views, render its child views
@ -120,12 +128,14 @@ class InnerViewItem extends StatelessWidget {
final children = childViews.map((childView) {
return ViewItem(
key: ValueKey('${categoryType.name} ${childView.id}'),
parentView: view,
categoryType: categoryType,
isFirstChild: childView.id == childViews.first.id,
view: childView,
level: level + 1,
onSelected: onSelected,
isDraggable: isDraggable,
leftPadding: leftPadding,
);
}).toList();
@ -139,7 +149,7 @@ class InnerViewItem extends StatelessWidget {
}
// wrap the child with DraggableItem if isDraggable is true
if (isDraggable) {
if (isDraggable && !isReferencedDatabaseView(view, parentView)) {
child = DraggableViewItem(
isFirstChild: isFirstChild,
view: view,
@ -147,10 +157,12 @@ class InnerViewItem extends StatelessWidget {
feedback: (context) {
return ViewItem(
view: view,
parentView: parentView,
categoryType: categoryType,
level: level,
onSelected: onSelected,
isDraggable: false,
leftPadding: leftPadding,
);
},
);
@ -170,19 +182,23 @@ class SingleInnerViewItem extends StatefulWidget {
const SingleInnerViewItem({
super.key,
required this.view,
required this.parentView,
required this.isExpanded,
required this.level,
this.leftPadding = 10,
required this.leftPadding,
this.isDraggable = true,
required this.showActions,
required this.onSelected,
});
final ViewPB view;
final ViewPB? parentView;
final bool isExpanded;
final int level;
final double leftPadding;
final bool isDraggable;
final bool showActions;
final void Function(ViewPB) onSelected;
@ -200,16 +216,16 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
buildWhenOnHover: () => !widget.showActions,
builder: (_, onHover) => _buildViewItem(onHover),
isSelected: () =>
widget.showActions ||
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id,
widget.isDraggable &&
(widget.showActions ||
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id),
);
}
Widget _buildViewItem(bool onHover) {
final children = [
// expand icon
_buildExpandedIcon(),
const HSpace(7),
_buildLeftIcon(),
// icon
SizedBox.square(
dimension: 16,
@ -229,12 +245,15 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
if (widget.showActions || onHover) {
// ··· more action button
children.add(_buildViewMoreActionButton(context));
// + button
children.add(_buildViewAddButton(context));
// only support add button for document layout
if (widget.view.layout == ViewLayoutPB.Document) {
// + button
children.add(_buildViewAddButton(context));
}
}
// Don't use GestureDetector here, because it doesn't response to the tap event sometimes.
return InkWell(
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => widget.onSelected(widget.view),
child: SizedBox(
height: 26,
@ -248,8 +267,14 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
);
}
// > button
Widget _buildExpandedIcon() {
// > button or · button
// show > if the view is expandable.
// show · if the view can't contain child views.
Widget _buildLeftIcon() {
if (isReferencedDatabaseView(widget.view, widget.parentView)) {
return const _DotIconWidget();
}
final name =
widget.isExpanded ? 'home/drop_down_show' : 'home/drop_down_hide';
return GestureDetector(
@ -343,3 +368,30 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
);
}
}
class _DotIconWidget extends StatelessWidget {
const _DotIconWidget();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(6.0),
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).iconTheme.color,
borderRadius: BorderRadius.circular(2),
),
),
);
}
}
// workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field.
bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
if (parentView == null) {
return false;
}
return view.layout.isDatabaseView && parentView.layout.isDatabaseView;
}

View File

@ -32,8 +32,8 @@ void main() {
menuBloc.add(const MenuEvent.createApp("App 3"));
await blocResponseFuture();
assert(menuBloc.state.views[0].name == 'App 3');
assert(menuBloc.state.views[1].name == 'App 2');
assert(menuBloc.state.views[2].name == 'App 1');
assert(menuBloc.state.views[1].name == 'App 1');
assert(menuBloc.state.views[2].name == 'App 2');
assert(menuBloc.state.views[3].name == 'App 3');
});
}