mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: insert reference page in nested page
This commit is contained in:
1
.github/workflows/flutter_ci.yaml
vendored
1
.github/workflows/flutter_ci.yaml
vendored
@ -134,6 +134,7 @@ jobs:
|
|||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
attempt_limit: 20
|
attempt_limit: 20
|
||||||
attempt_delay: 10000
|
attempt_delay: 10000
|
||||||
|
|
||||||
|
1
.github/workflows/integration_test.yml
vendored
1
.github/workflows/integration_test.yml
vendored
@ -136,5 +136,6 @@ jobs:
|
|||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
attempt_limit: 20
|
attempt_limit: 20
|
||||||
attempt_delay: 10000
|
attempt_delay: 10000
|
@ -1,9 +1,12 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||||
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
|
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/plugins/database_view/grid/presentation/grid_page.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.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';
|
||||||
@ -61,6 +64,54 @@ void main() {
|
|||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('create a grid inside a document', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await createInlineDatabase(tester, ViewLayoutPB.Grid);
|
||||||
|
|
||||||
|
// validate the referenced grid is inserted
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(AppFlowyEditor),
|
||||||
|
matching: find.byType(GridPage),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create a board inside a document', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await createInlineDatabase(tester, ViewLayoutPB.Board);
|
||||||
|
|
||||||
|
// validate the referenced grid is inserted
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(AppFlowyEditor),
|
||||||
|
matching: find.byType(BoardPage),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('create a calendar inside a document', (tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
await createInlineDatabase(tester, ViewLayoutPB.Calendar);
|
||||||
|
|
||||||
|
// validate the referenced grid is inserted
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(AppFlowyEditor),
|
||||||
|
matching: find.byType(CalendarPage),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,11 +126,13 @@ Future<void> insertReferenceDatabase(
|
|||||||
await tester.createNewPageWithName(
|
await tester.createNewPageWithName(
|
||||||
name: name,
|
name: name,
|
||||||
layout: layout,
|
layout: layout,
|
||||||
|
openAfterCreated: false,
|
||||||
);
|
);
|
||||||
// create a new document
|
// create a new document
|
||||||
await tester.createNewPageWithName(
|
await tester.createNewPageWithName(
|
||||||
name: 'insert_a_reference_${layout.name}',
|
name: 'insert_a_reference_${layout.name}',
|
||||||
layout: ViewLayoutPB.Document,
|
layout: ViewLayoutPB.Document,
|
||||||
|
openAfterCreated: true,
|
||||||
);
|
);
|
||||||
// tap the first line of the document
|
// tap the first line of the document
|
||||||
await tester.editor.tapLineOfEditorAt(0);
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
@ -98,3 +151,38 @@ Future<void> insertReferenceDatabase(
|
|||||||
expect(referencedDatabase, findsOneWidget);
|
expect(referencedDatabase, findsOneWidget);
|
||||||
await tester.tapButton(referencedDatabase);
|
await tester.tapButton(referencedDatabase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> createInlineDatabase(
|
||||||
|
WidgetTester tester,
|
||||||
|
ViewLayoutPB layout,
|
||||||
|
) async {
|
||||||
|
// create a new document
|
||||||
|
final documentName = 'insert_a_inline_${layout.name}';
|
||||||
|
await tester.createNewPageWithName(
|
||||||
|
name: documentName,
|
||||||
|
layout: ViewLayoutPB.Document,
|
||||||
|
openAfterCreated: true,
|
||||||
|
);
|
||||||
|
// tap the first line of the document
|
||||||
|
await tester.editor.tapLineOfEditorAt(0);
|
||||||
|
// insert a referenced view
|
||||||
|
await tester.editor.showSlashMenu();
|
||||||
|
final name = switch (layout) {
|
||||||
|
ViewLayoutPB.Grid => LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
|
||||||
|
ViewLayoutPB.Board =>
|
||||||
|
LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
|
||||||
|
ViewLayoutPB.Calendar =>
|
||||||
|
LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr(),
|
||||||
|
_ => '',
|
||||||
|
};
|
||||||
|
await tester.editor.tapSlashMenuItemWithName(
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final childViews = tester
|
||||||
|
.widget<SingleInnerViewItem>(tester.findPageName(documentName))
|
||||||
|
.view
|
||||||
|
.childViews;
|
||||||
|
expect(childViews.length, 1);
|
||||||
|
}
|
||||||
|
@ -234,13 +234,19 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration.copyWith(
|
||||||
|
padding: (_) => const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
|
@ -82,23 +82,16 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|||||||
final _focusNode = FocusNode(debugLabel: 'reference_list_widget');
|
final _focusNode = FocusNode(debugLabel: 'reference_list_widget');
|
||||||
EditorStyle get style => widget.editorState.editorStyle;
|
EditorStyle get style => widget.editorState.editorStyle;
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
int _totalItems = 0;
|
final int _totalItems = 0;
|
||||||
Future<List<(ViewPB, List<ViewPB>)>>? _availableLayout;
|
Future<List<ViewPB>>? _availableLayout;
|
||||||
final Map<int, (ViewPB, ViewPB)> _items = {};
|
final List<ViewPB> _items = [];
|
||||||
|
|
||||||
Future<List<(ViewPB, List<ViewPB>)>> fetchItems() async {
|
Future<List<ViewPB>> fetchItems() async {
|
||||||
final items =
|
final items =
|
||||||
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
|
await ViewBackendService().fetchViewsWithLayoutType(widget.layoutType);
|
||||||
|
_items
|
||||||
int index = 0;
|
..clear()
|
||||||
for (final (app, children) in items) {
|
..addAll(items);
|
||||||
for (final view in children) {
|
|
||||||
_items.putIfAbsent(index, () => (app, view));
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_totalItems = _items.length;
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,8 +169,8 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|||||||
newSelectedIndex %= _totalItems;
|
newSelectedIndex %= _totalItems;
|
||||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||||
widget.onSelected(
|
widget.onSelected(
|
||||||
_items[_selectedIndex]!.$1,
|
_items[_selectedIndex],
|
||||||
_items[_selectedIndex]!.$2,
|
_items[_selectedIndex],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,10 +184,10 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|||||||
Widget _buildListWidget(
|
Widget _buildListWidget(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
int selectedIndex,
|
int selectedIndex,
|
||||||
Future<List<(ViewPB, List<ViewPB>)>>? items,
|
Future<List<ViewPB>>? items,
|
||||||
) {
|
) {
|
||||||
int index = 0;
|
int index = 0;
|
||||||
return FutureBuilder<List<(ViewPB, List<ViewPB>)>>(
|
return FutureBuilder<List<ViewPB>>(
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData &&
|
if (snapshot.hasData &&
|
||||||
snapshot.connectionState == ConnectionState.done) {
|
snapshot.connectionState == ConnectionState.done) {
|
||||||
@ -211,35 +204,23 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (views != null && views.isNotEmpty) {
|
if (views != null && views.isNotEmpty) {
|
||||||
for (final (view, viewChildren) in views) {
|
for (final view in views) {
|
||||||
if (viewChildren.isNotEmpty) {
|
|
||||||
children.add(
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: FlowyText.regular(
|
|
||||||
view.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final value in viewChildren) {
|
|
||||||
children.add(
|
children.add(
|
||||||
FlowyButton(
|
FlowyButton(
|
||||||
isSelected: index == _selectedIndex,
|
isSelected: index == _selectedIndex,
|
||||||
leftIcon: svgWidget(
|
leftIcon: svgWidget(
|
||||||
value.iconName,
|
view.iconName,
|
||||||
color: Theme.of(context).iconTheme.color,
|
color: Theme.of(context).iconTheme.color,
|
||||||
),
|
),
|
||||||
text: FlowyText.regular(value.name),
|
text: FlowyText.regular(view.name),
|
||||||
onTap: () => widget.onSelected(view, value),
|
onTap: () => widget.onSelected(view, view),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: children,
|
children: children,
|
||||||
|
@ -82,6 +82,11 @@ class _DatabaseBlockComponentWidgetState
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
child = Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
if (widget.actionBuilder != null) {
|
if (widget.actionBuilder != null) {
|
||||||
child = BlockComponentActionWrapper(
|
child = BlockComponentActionWrapper(
|
||||||
node: widget.node,
|
node: widget.node,
|
||||||
|
@ -17,11 +17,8 @@ SelectionMenuItem inlineGridMenuItem(DocumentBloc documentBloc) =>
|
|||||||
),
|
),
|
||||||
keywords: ['grid', 'database'],
|
keywords: ['grid', 'database'],
|
||||||
handler: (editorState, menuService, context) async {
|
handler: (editorState, menuService, context) async {
|
||||||
if (!documentBloc.view.hasParentViewId()) {
|
// create the view inside current page
|
||||||
return;
|
final parentViewId = documentBloc.view.id;
|
||||||
}
|
|
||||||
|
|
||||||
final parentViewId = documentBloc.view.parentViewId;
|
|
||||||
ViewBackendService.createView(
|
ViewBackendService.createView(
|
||||||
parentViewId: parentViewId,
|
parentViewId: parentViewId,
|
||||||
openAfterCreate: false,
|
openAfterCreate: false,
|
||||||
@ -45,11 +42,8 @@ SelectionMenuItem inlineBoardMenuItem(DocumentBloc documentBloc) =>
|
|||||||
),
|
),
|
||||||
keywords: ['board', 'kanban', 'database'],
|
keywords: ['board', 'kanban', 'database'],
|
||||||
handler: (editorState, menuService, context) async {
|
handler: (editorState, menuService, context) async {
|
||||||
if (!documentBloc.view.hasParentViewId()) {
|
// create the view inside current page
|
||||||
return;
|
final parentViewId = documentBloc.view.id;
|
||||||
}
|
|
||||||
|
|
||||||
final parentViewId = documentBloc.view.parentViewId;
|
|
||||||
ViewBackendService.createView(
|
ViewBackendService.createView(
|
||||||
parentViewId: parentViewId,
|
parentViewId: parentViewId,
|
||||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
@ -72,11 +66,8 @@ SelectionMenuItem inlineCalendarMenuItem(DocumentBloc documentBloc) =>
|
|||||||
),
|
),
|
||||||
keywords: ['calendar', 'database'],
|
keywords: ['calendar', 'database'],
|
||||||
handler: (editorState, menuService, context) async {
|
handler: (editorState, menuService, context) async {
|
||||||
if (!documentBloc.view.hasParentViewId()) {
|
// create the view inside current page
|
||||||
return;
|
final parentViewId = documentBloc.view.id;
|
||||||
}
|
|
||||||
|
|
||||||
final parentViewId = documentBloc.view.parentViewId;
|
|
||||||
ViewBackendService.createView(
|
ViewBackendService.createView(
|
||||||
parentViewId: parentViewId,
|
parentViewId: parentViewId,
|
||||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||||
|
@ -2,7 +2,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selec
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
enum MentionType {
|
enum MentionType {
|
||||||
@ -98,17 +97,11 @@ class InlinePageReferenceService {
|
|||||||
|
|
||||||
Future<List<SelectionMenuItem>> generatePageItems(String character) async {
|
Future<List<SelectionMenuItem>> generatePageItems(String character) async {
|
||||||
final service = ViewBackendService();
|
final service = ViewBackendService();
|
||||||
final List<(ViewPB, List<ViewPB>)> pbViews = await service.fetchViews(
|
final views = await service.fetchViews();
|
||||||
(_, __) => true,
|
if (views.isEmpty) {
|
||||||
);
|
|
||||||
if (pbViews.isEmpty) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final List<SelectionMenuItem> pages = [];
|
final List<SelectionMenuItem> pages = [];
|
||||||
final List<ViewPB> views = [];
|
|
||||||
for (final element in pbViews) {
|
|
||||||
views.addAll(element.$2);
|
|
||||||
}
|
|
||||||
views.sort(((a, b) => b.createTime.compareTo(a.createTime)));
|
views.sort(((a, b) => b.createTime.compareTo(a.createTime)));
|
||||||
|
|
||||||
for (final view in views) {
|
for (final view in views) {
|
||||||
|
@ -173,41 +173,51 @@ class ViewBackendService {
|
|||||||
return FolderEventMoveNestedView(payload).send();
|
return FolderEventMoveNestedView(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<(ViewPB, List<ViewPB>)>> fetchViewsWithLayoutType(
|
Future<List<ViewPB>> fetchViewsWithLayoutType(
|
||||||
ViewLayoutPB? layoutType,
|
ViewLayoutPB? layoutType,
|
||||||
) async {
|
) async {
|
||||||
return fetchViews((workspace, view) {
|
final views = await fetchViews();
|
||||||
if (layoutType != null) {
|
if (layoutType == null) {
|
||||||
return view.layout == layoutType;
|
return views;
|
||||||
}
|
}
|
||||||
return true;
|
return views
|
||||||
});
|
.where(
|
||||||
|
(element) => layoutType == element.layout,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<(ViewPB, List<ViewPB>)>> fetchViews(
|
Future<List<ViewPB>> fetchViews() async {
|
||||||
bool Function(WorkspaceSettingPB workspace, ViewPB view) filter,
|
final result = <ViewPB>[];
|
||||||
) async {
|
|
||||||
final result = <(ViewPB, List<ViewPB>)>[];
|
|
||||||
return FolderEventGetCurrentWorkspace().send().then((value) async {
|
return FolderEventGetCurrentWorkspace().send().then((value) async {
|
||||||
final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
|
final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
|
||||||
if (workspaces != null) {
|
if (workspaces != null) {
|
||||||
final views = workspaces.workspace.views;
|
final views = workspaces.workspace.views;
|
||||||
for (final view in views) {
|
for (final view in views) {
|
||||||
final childViews = await getChildViews(viewId: view.id).then(
|
result.add(view);
|
||||||
(value) => value
|
final childViews = await getAllViews(view);
|
||||||
.getLeftOrNull<List<ViewPB>>()
|
result.addAll(childViews);
|
||||||
?.where((e) => filter(workspaces, e))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
if (childViews != null && childViews.isNotEmpty) {
|
|
||||||
result.add((view, childViews));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<ViewPB>> getAllViews(ViewPB view) async {
|
||||||
|
final result = <ViewPB>[];
|
||||||
|
final childViews = await getChildViews(viewId: view.id).then(
|
||||||
|
(value) => value.getLeftOrNull<List<ViewPB>>()?.toList(),
|
||||||
|
);
|
||||||
|
if (childViews != null && childViews.isNotEmpty) {
|
||||||
|
result.addAll(childViews);
|
||||||
|
final views = await Future.wait(
|
||||||
|
childViews.map((e) async => await getAllViews(e)),
|
||||||
|
);
|
||||||
|
result.addAll(views.expand((element) => element));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Either<ViewPB, FlowyError>> getView(
|
static Future<Either<ViewPB, FlowyError>> getView(
|
||||||
String viewID,
|
String viewID,
|
||||||
) async {
|
) async {
|
||||||
|
Reference in New Issue
Block a user