mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-11-10 10:18:57 +03:00
feat: disable moving page into the database (#3107)
This commit is contained in:
parent
135d8a811e
commit
fb9bc359b8
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user