feat: implement draggable folder (#3083)

This commit is contained in:
Lucas.Xu 2023-07-31 19:06:01 +07:00 committed by GitHub
parent eb77346e5a
commit 266209caeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 2170 additions and 249 deletions

View File

@ -14,8 +14,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Calendar);
// open setting
await tester.tapDatabaseSettingButton();
@ -36,7 +35,11 @@ void main() {
await tester.tapGoButton();
// Create calendar view
await tester.createNewPageWithName(ViewLayoutPB.Calendar, 'calendar');
const name = 'calendar';
await tester.createNewPageWithName(
name: name,
layout: ViewLayoutPB.Calendar,
);
// Open setting
await tester.tapDatabaseSettingButton();
@ -47,9 +50,9 @@ void main() {
await tester.tapFirstDayOfWeekStartFromMonday();
// Open the other page and open the new calendar page again
await tester.openPage(readme);
await tester.openPage(gettingStated);
await tester.pumpAndSettle(const Duration(milliseconds: 300));
await tester.openPage('calendar');
await tester.openPage(name, layout: ViewLayoutPB.Calendar);
// Open setting again and check the start from Monday is selected
await tester.tapDatabaseSettingButton();
@ -65,8 +68,7 @@ void main() {
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Calendar);
// Scroll until today's date cell is visible
await tester.scrollToToday();
@ -135,8 +137,7 @@ void main() {
await tester.tapGoButton();
// Create the calendar view
await tester.tapAddButton();
await tester.tapCreateCalendarButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Calendar);
// Create a new event on the first of this month
final today = DateTime.now();

View File

@ -15,8 +15,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.editCell(
rowIndex: 0,
@ -38,7 +37,10 @@ void main() {
testWidgets('edit multiple text cells', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid, 'my grid');
await tester.createNewPageWithName(
name: 'my grid',
layout: ViewLayoutPB.Grid,
);
await tester.createField(FieldType.RichText, 'description');
await tester.editCell(
@ -75,8 +77,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Number;
@ -134,8 +135,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.assertCheckboxCell(rowIndex: 0, isSelected: false);
await tester.tapCheckboxCellInGrid(rowIndex: 0);
@ -153,8 +153,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.CreatedTime;
// Create a create time field
@ -172,8 +171,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.LastEditedTime;
// Create a last time field
@ -191,8 +189,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.DateTime;
await tester.createField(fieldType, fieldType.name);
@ -288,9 +285,9 @@ void main() {
await tester.tapGoButton();
const fieldType = FieldType.SingleSelect;
await tester.tapAddButton();
// When create a grid, it will create a single select field by default
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
@ -366,8 +363,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.MultiSelect;
await tester.createField(fieldType, fieldType.name);

View File

@ -17,8 +17,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Name');
@ -35,8 +34,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');
@ -58,8 +56,7 @@ void main() {
await tester.tapGoButton();
// create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// create a field
await tester.createField(FieldType.Checklist, 'checklist');
@ -73,8 +70,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// create a field
await tester.createField(FieldType.Checkbox, 'New field 1');
@ -94,8 +90,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
@ -115,8 +110,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// create a field
await tester.scrollToRight(find.byType(GridPage));
@ -136,8 +130,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.scrollToRight(find.byType(GridPage));
await tester.tapNewPropertyButton();
@ -157,8 +150,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
for (final fieldType in [
FieldType.Checklist,
@ -190,7 +182,9 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.createNewPageWithName(ViewLayoutPB.Grid);
await tester.createNewPageWithName(
layout: ViewLayoutPB.Grid,
);
// Invoke the field editor
await tester.tapGridFieldWithName('Type');

View File

@ -9,7 +9,7 @@ import 'util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('grid', () {
group('database filter', () {
testWidgets('add text filter', (tester) async {
await tester.openV020database();

View File

@ -1,5 +1,6 @@
import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
@ -19,8 +20,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -34,8 +34,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -55,8 +54,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -85,8 +83,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -108,8 +105,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -144,8 +140,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -160,8 +155,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -201,8 +195,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -241,8 +234,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();
@ -258,8 +250,7 @@ void main() {
await tester.tapGoButton();
// Create a new grid
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Hover first row and then open the row page
await tester.openFirstRowDetailPage();

View File

@ -1,3 +1,4 @@
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -12,8 +13,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.tapCreateRowButtonInGrid();
// The initial number of rows is 3
@ -25,8 +25,8 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid();
await tester.tapCreateRowButtonInRowMenuOfGrid();
@ -41,8 +41,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.hoverOnFirstRowOfGrid();
// Open the row menu and then click the delete
@ -60,8 +59,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
await tester.assertRowCountInGridPage(3);
await tester.pumpAndSettle();

View File

@ -1,4 +1,5 @@
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -13,8 +14,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// open setting
await tester.tapDatabaseSettingButton();
@ -31,8 +31,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// open setting
await tester.tapDatabaseSettingButton();

View File

@ -15,8 +15,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
@ -37,8 +36,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);
@ -63,8 +61,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapGoButton();
await tester.tapAddButton();
await tester.tapCreateGridButton();
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
// Create board view
await tester.tapCreateLinkedDatabaseViewButton(AddButtonAction.board);

View File

@ -17,9 +17,7 @@ void main() {
await tester.tapGoButton();
// create a new document
await tester.tapAddButton();
await tester.tapCreateDocumentButton();
await tester.pumpAndSettle();
await tester.createNewPageWithName();
// expect to see a new document
tester.expectToSeePageName(
@ -35,19 +33,21 @@ void main() {
await tester.tapGoButton();
// delete the readme page
await tester.hoverOnPageName(readme);
await tester.tapDeletePageButton();
await tester.hoverOnPageName(
gettingStated,
onHover: () async => await tester.tapDeletePageButton(),
);
// the banner should show up and the readme page should be gone
tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(readme);
tester.expectNotToSeePageName(gettingStated);
// restore the readme page
await tester.tapRestoreButton();
// the banner should be gone and the readme page should be back
tester.expectNotToSeeDocumentBanner();
tester.expectToSeePageName(readme);
tester.expectToSeePageName(gettingStated);
});
testWidgets('delete the readme page and delete it permanently',
@ -57,19 +57,21 @@ void main() {
await tester.tapGoButton();
// delete the readme page
await tester.hoverOnPageName(readme);
await tester.tapDeletePageButton();
await tester.hoverOnPageName(
gettingStated,
onHover: () async => await tester.tapDeletePageButton(),
);
// the banner should show up and the readme page should be gone
tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(readme);
tester.expectNotToSeePageName(gettingStated);
// delete the page permanently
await tester.tapDeletePermanentlyButton();
// the banner should be gone and the readme page should be gone
tester.expectNotToSeeDocumentBanner();
tester.expectNotToSeePageName(readme);
tester.expectNotToSeePageName(gettingStated);
});
});
}

View File

@ -73,13 +73,13 @@ Future<void> insertReferenceDatabase(
final id = uuid();
final name = '${layout.name}_$id';
await tester.createNewPageWithName(
layout,
name,
name: name,
layout: layout,
);
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'insert_a_reference_${layout.name}',
name: 'insert_a_reference_${layout.name}',
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);

View File

@ -21,8 +21,8 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
name: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
@ -67,8 +67,8 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
name: LocaleKeys.document_plugins_createInlineMathEquation.tr(),
layout: ViewLayoutPB.Document,
);
// tap the first line of the document

View File

@ -62,9 +62,11 @@ void main() {
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Document);
// rename
await tester.hoverOnPageName(pageName);
const newName = 'RenameToNewPageName';
await tester.renamePage(newName);
await tester.hoverOnPageName(
pageName,
onHover: () async => await tester.renamePage(newName),
);
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(newName),
@ -79,8 +81,11 @@ void main() {
final pageName = await insertingInlinePage(tester, ViewLayoutPB.Grid);
// rename
await tester.hoverOnPageName(pageName);
await tester.tapDeletePageButton();
await tester.hoverOnPageName(
pageName,
layout: ViewLayoutPB.Grid,
onHover: () async => await tester.tapDeletePageButton(),
);
final finder = find.descendant(
of: find.byType(MentionPageBlock),
matching: find.findTextInFlowyText(pageName),
@ -101,13 +106,14 @@ Future<String> insertingInlinePage(
final id = uuid();
final name = '${layout.name}_$id';
await tester.createNewPageWithName(
layout,
name,
name: name,
layout: layout,
openAfterCreated: false,
);
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'insert_a_inline_page_${layout.name}',
name: 'insert_a_inline_page_${layout.name}',
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);

View File

@ -25,7 +25,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
layout: ViewLayoutPB.Document,
);
// tap the first line of the document

View File

@ -16,8 +16,8 @@ void main() {
await tester.tapGoButton();
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'outline_test',
name: 'outline_test',
layout: ViewLayoutPB.Document,
);
await tester.editor.tapLineOfEditorAt(0);
@ -33,8 +33,8 @@ void main() {
await tester.tapGoButton();
await tester.createNewPageWithName(
ViewLayoutPB.Document,
'outline_test',
name: 'outline_test',
layout: ViewLayoutPB.Document,
);
await tester.editor.tapLineOfEditorAt(0);

View File

@ -32,7 +32,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
@ -78,7 +78,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
@ -118,7 +118,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
@ -156,7 +156,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
layout: ViewLayoutPB.Document,
);
// tap the first line of the document
@ -191,7 +191,7 @@ void main() {
// create a new document
await tester.createNewPageWithName(
ViewLayoutPB.Document,
layout: ViewLayoutPB.Document,
);
// tap the first line of the document

View File

@ -18,7 +18,10 @@ void main() {
// create a new document called Sample
const pageName = 'Sample';
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
await tester.createNewPageWithName(
name: pageName,
layout: ViewLayoutPB.Document,
);
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
@ -56,7 +59,7 @@ void main() {
);
// switch to other page and switch back
await tester.openPage(readme);
await tester.openPage(gettingStated);
await tester.openPage(pageName);
// the numbered list should be kept
@ -72,7 +75,10 @@ void main() {
// create a new document called Sample
const pageName = 'Sample';
await tester.createNewPageWithName(ViewLayoutPB.Document, pageName);
await tester.createNewPageWithName(
name: pageName,
layout: ViewLayoutPB.Document,
);
// focus on the editor
await tester.editor.tapLineOfEditorAt(0);
@ -85,7 +91,7 @@ void main() {
}
// switch to other page and switch back
await tester.openPage(readme);
await tester.openPage(gettingStated);
await tester.openPage(pageName);
// this screenshots are different on different platform, so comment it out temporarily.

View File

@ -16,10 +16,10 @@ void main() {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
// expect to see a getting started page
tester.expectToSeePageName(gettingStated);
await tester.tapAddButton();
await tester.tapAddViewButton();
await tester.tapImportButton();
final testFileNames = ['test1.md', 'test2.md'];

View File

@ -14,6 +14,7 @@ import 'document/document_test_runner.dart' as document_test_runner;
import 'import_files_test.dart' as import_files_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'switch_folder_test.dart' as switch_folder_test;
import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
/// The main task runner for all integration tests in AppFlowy.
///
@ -31,6 +32,9 @@ void main() {
// Document integration tests
document_test_runner.startTesting();
// Sidebar integration tests
sidebar_test_runner.startTesting();
// Database integration tests
database_cell_test.main();
database_field_test.main();

View File

@ -16,7 +16,7 @@ void main() {
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
tester.expectToSeePageName(gettingStated);
// mock the file picker
final path = await mockSaveFilePath(
@ -42,12 +42,16 @@ void main() {
final context = await tester.initializeAppFlowy();
await tester.tapGoButton();
// expect to see a readme page
tester.expectToSeePageName(readme);
// expect to see a getting started page
tester.expectToSeePageName(gettingStated);
// rename the document
await tester.hoverOnPageName(readme);
await tester.renamePage('example');
await tester.hoverOnPageName(
gettingStated,
onHover: () async {
await tester.renamePage('example');
},
);
final shareButton = find.byType(ShareActionList);
final shareButtonState =

View File

@ -0,0 +1,142 @@
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/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_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../util/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('sidebar test', () {
testWidgets('create a new page', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
// create a new page
const name = 'Hello AppFlowy';
await tester.tapNewPageButton();
await tester.enterText(find.byType(TextFormField), name);
await tester.tapOKButton();
// expect to see a new document
tester.expectToSeePageName(
name,
);
// and with one paragraph block
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
});
testWidgets('create a new document, grid, board and calendar',
(tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
for (final layout in ViewLayoutPB.values) {
// create a new page
final name = 'AppFlowy_$layout';
await tester.createNewPageWithName(
name: name,
layout: layout,
);
// expect to see a new page
tester.expectToSeePageName(
name,
layout: layout,
);
switch (layout) {
case ViewLayoutPB.Document:
// and with one paragraph block
expect(find.byType(TextBlockComponentWidget), findsOneWidget);
break;
case ViewLayoutPB.Grid:
expect(find.byType(GridPage), findsOneWidget);
break;
case ViewLayoutPB.Board:
expect(find.byType(BoardPage), findsOneWidget);
break;
case ViewLayoutPB.Calendar:
expect(find.byType(CalendarPage), findsOneWidget);
break;
}
await tester.openPage(gettingStated);
}
});
testWidgets('create some nested pages, and move them', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
final names = [1, 2, 3, 4].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1];
await tester.createNewPageWithName(
name: names[i],
parentName: parentName,
layout: ViewLayoutPB.Document,
);
tester.expectToSeePageName(names[i], parentName: parentName);
}
// move the document_3 to the getting started page
await tester.movePageToOtherPage(
name: names[3],
parentName: gettingStated,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document,
);
final fromId = tester
.widget<SingleInnerViewItem>(tester.findPageName(names[3]))
.view
.parentViewId;
final toId = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.view
.id;
expect(fromId, toId);
// move the document_2 before document_1
await tester.movePageToOtherPage(
name: names[2],
parentName: gettingStated,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document,
position: DraggableHoverPosition.bottom,
);
final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.view
.childViews;
expect(
childViews[0].id,
tester
.widget<SingleInnerViewItem>(tester.findPageName(names[2]))
.view
.id,
);
expect(
childViews[1].id,
tester
.widget<SingleInnerViewItem>(tester.findPageName(names[0]))
.view
.id,
);
expect(
childViews[2].id,
tester
.widget<SingleInnerViewItem>(tester.findPageName(names[3]))
.view
.id,
);
});
});
}

View File

@ -0,0 +1,10 @@
import 'package:integration_test/integration_test.dart';
import 'sidebar_test.dart' as sidebar_test;
void startTesting() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Sidebar integration tests
sidebar_test.main();
}

View File

@ -29,8 +29,14 @@ void main() {
findsNothing,
);
await tester.createNewPageWithName(ViewLayoutPB.Calendar, _calendarName);
await tester.createNewPageWithName(ViewLayoutPB.Document, _documentName);
await tester.createNewPageWithName(
name: _calendarName,
layout: ViewLayoutPB.Calendar,
);
await tester.createNewPageWithName(
name: _documentName,
layout: ViewLayoutPB.Document,
);
// Navigate current view to "Read me" document again
await tester.tapButtonWithName(_readmeName);

View File

@ -91,7 +91,6 @@ extension AppFlowyTestBase on WidgetTester {
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
}
Future<void> tapButtonWithName(

View File

@ -1,10 +1,14 @@
import 'dart:ui';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@ -24,35 +28,48 @@ extension CommonOperations on WidgetTester {
}
/// Tap the + button on the home page.
Future<void> tapAddButton() async {
final addButton = find.byType(AddButton);
await tapButton(addButton);
Future<void> tapAddViewButton({
String name = gettingStated,
}) async {
await hoverOnPageName(
name,
onHover: () async {
final addButton = find.byType(ViewAddButton);
await tapButton(addButton);
},
);
}
/// Tap the 'New Page' Button on the sidebar.
Future<void> tapNewPageButton() async {
final newPageButton = find.byType(SidebarNewPageButton);
await tapButton(newPageButton);
}
/// Tap the create document button.
///
/// Must call [tapAddButton] first.
/// Must call [tapAddViewButton] first.
Future<void> tapCreateDocumentButton() async {
await tapButtonWithName(LocaleKeys.document_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddButton] first.
/// Must call [tapAddViewButton] first.
Future<void> tapCreateGridButton() async {
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddButton] first.
/// Must call [tapAddViewButton] first.
Future<void> tapCreateCalendarButton() async {
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
}
/// Tap the import button.
///
/// Must call [tapAddButton] first.
/// Must call [tapAddViewButton] first.
Future<void> tapImportButton() async {
await tapButtonWithName(LocaleKeys.moreAction_import.tr());
}
@ -116,6 +133,7 @@ extension CommonOperations on WidgetTester {
Finder finder, {
Offset? offset,
Future<void> Function()? onHover,
bool removePointer = true,
}) async {
try {
final gesture = await createGesture(kind: PointerDeviceKind.mouse);
@ -133,19 +151,30 @@ extension CommonOperations on WidgetTester {
/// Hover on the page name.
Future<void> hoverOnPageName(
String name, {
ViewLayoutPB layout = ViewLayoutPB.Document,
Future<void> Function()? onHover,
bool useLast = true,
}) async {
final pageNames = findPageName(name, layout: layout);
if (useLast) {
await hoverOnWidget(findPageName(name).last, onHover: onHover);
await hoverOnWidget(
pageNames.last,
onHover: onHover,
);
} else {
await hoverOnWidget(findPageName(name).first, onHover: onHover);
await hoverOnWidget(
pageNames.first,
onHover: onHover,
);
}
}
/// open the page with given name.
Future<void> openPage(String name) async {
final finder = findPageName(name);
Future<void> openPage(
String name, {
ViewLayoutPB layout = ViewLayoutPB.Document,
}) async {
final finder = findPageName(name, layout: layout);
expect(finder, findsOneWidget);
await tapButton(finder);
}
@ -154,20 +183,20 @@ extension CommonOperations on WidgetTester {
///
/// Must call [hoverOnPageName] first.
Future<void> tapPageOptionButton() async {
final optionButton = find.byType(ViewDisclosureButton);
final optionButton = find.byType(ViewMoreActionButton);
await tapButton(optionButton);
}
/// Tap the delete page button.
Future<void> tapDeletePageButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewDisclosureAction.delete.name);
await tapButtonWithName(ViewMoreActionType.delete.name);
}
/// Tap the rename page button.
Future<void> tapRenamePageButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewDisclosureAction.rename.name);
await tapButtonWithName(ViewMoreActionType.rename.name);
}
/// Rename the page.
@ -224,12 +253,14 @@ extension CommonOperations on WidgetTester {
await tapButton(markdownButton);
}
Future<void> createNewPageWithName(
ViewLayoutPB layout, [
Future<void> createNewPageWithName({
String? name,
]) async {
ViewLayoutPB layout = ViewLayoutPB.Document,
String? parentName,
bool openAfterCreated = true,
}) async {
// create a new page
await tapAddButton();
await tapAddViewButton(name: parentName ?? gettingStated);
await tapButtonWithName(layout.menuName);
await pumpAndSettle();
@ -237,6 +268,7 @@ extension CommonOperations on WidgetTester {
if (name != null) {
await hoverOnPageName(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
onHover: () async {
await renamePage(name);
await pumpAndSettle();
@ -244,6 +276,16 @@ extension CommonOperations on WidgetTester {
);
await pumpAndSettle();
}
// open the page after created
if (openAfterCreated) {
await openPage(
// if the name is null, use the default name
name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
);
await pumpAndSettle();
}
}
Future<void> simulateKeyEvent(
@ -289,6 +331,34 @@ extension CommonOperations on WidgetTester {
await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr()));
await pumpAndSettle();
}
Future<void> movePageToOtherPage({
required String name,
required String parentName,
required ViewLayoutPB layout,
required ViewLayoutPB parentLayout,
DraggableHoverPosition position = DraggableHoverPosition.center,
}) async {
final from = findPageName(name, layout: layout);
final to = findPageName(parentName, layout: parentLayout);
final gesture = await startGesture(getCenter(from));
Offset offset = Offset.zero;
switch (position) {
case DraggableHoverPosition.center:
offset = getCenter(to);
break;
case DraggableHoverPosition.top:
offset = getTopLeft(to);
break;
case DraggableHoverPosition.bottom:
offset = getBottomLeft(to);
break;
default:
}
await gesture.moveTo(offset);
await gesture.up();
await pumpAndSettle();
}
}
extension ViewLayoutPBTest on ViewLayoutPB {

View File

@ -76,9 +76,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapGoButton();
// expect to see a readme page
expectToSeePageName(readme);
expectToSeePageName(gettingStated);
await tapAddButton();
await tapAddViewButton();
await tapImportButton();
final testFileNames = ['v020.afdb'];
@ -102,7 +102,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
paths: paths,
);
await tapDatabaseRawDataButton();
await openPage('v020');
await pumpAndSettle();
await openPage('v020', layout: ViewLayoutPB.Grid);
}
Future<void> hoverOnFirstRowOfGrid() async {

View File

@ -3,29 +3,51 @@ import 'package:appflowy/plugins/document/presentation/banner.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
const String readme = 'Read me';
// const String readme = 'Read me';
const String gettingStated = '⭐️ Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
void expectToSeeHomePage() {
expect(find.byType(HomeStack), findsOneWidget);
expect(find.textContaining(readme), findsWidgets);
expect(find.textContaining(gettingStated), findsWidgets);
}
/// Expect to see the page name on the home page.
void expectToSeePageName(String name) {
final pageName = findPageName(name);
void expectToSeePageName(
String name, {
String? parentName,
ViewLayoutPB layout = ViewLayoutPB.Document,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
final pageName = findPageName(
name,
layout: layout,
parentName: parentName,
parentLayout: parentLayout,
);
expect(pageName, findsOneWidget);
}
/// Expect not to see the page name on the home page.
void expectNotToSeePageName(String name) {
final pageName = findPageName(name);
void expectNotToSeePageName(
String name, {
String? parentName,
ViewLayoutPB layout = ViewLayoutPB.Document,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
final pageName = findPageName(
name,
layout: layout,
parentName: parentName,
parentLayout: parentLayout,
);
expect(pageName, findsNothing);
}
@ -126,10 +148,31 @@ extension Expectation on WidgetTester {
}
/// Find the page name on the home page.
Finder findPageName(String name) {
return find.byWidgetPredicate(
(widget) => widget is ViewSectionItem && widget.view.name == name,
skipOffstage: false,
Finder findPageName(
String name, {
ViewLayoutPB layout = ViewLayoutPB.Document,
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
if (parentName == null) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
return find.descendant(
of: find.byWidgetPredicate(
(widget) =>
widget is ViewItem &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
),
matching: findPageName(name),
);
}
}

View File

@ -29,4 +29,10 @@ class KVKeys {
'kDocumentAppearanceFontSize';
static const String kDocumentAppearanceFontFamily =
'kDocumentAppearanceFontFamily';
/// The key for saving the expanded views
///
/// The value is a json string with the following format:
/// {'viewId': true, 'viewId2': false}
static const String expandedViews = 'expandedViews';
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
@ -109,6 +110,11 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
return;
}
getIt<MenuSharedState>().latestOpenView = view;
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: view.plugin(),
),
);
}
Future<ViewPB?> fetchView(String pageId) async {

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/workspace/workspace_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@ -43,7 +44,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
desc: event.desc ?? "",
);
result.fold(
(app) => {},
(app) => emit(state.copyWith(plugin: app.plugin())),
(error) {
Log.error(error);
emit(state.copyWith(successOrFailure: right(error)));

View File

@ -1,3 +1,8 @@
import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:dartz/dartz.dart';
@ -20,21 +25,34 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
super(ViewState.init(view)) {
on<ViewEvent>((event, emit) async {
await event.map(
initial: (e) {
initial: (e) async {
listener.start(
onViewUpdated: (result) {
add(ViewEvent.viewDidUpdate(left(result)));
},
);
emit(state);
final isExpanded = await _getViewIsExpanded(view);
await _loadViewsWhenExpanded(emit, isExpanded);
},
setIsEditing: (e) {
emit(state.copyWith(isEditing: e.isEditing));
},
setIsExpanded: (e) async {
if (e.isExpanded) {
await _loadViewsWhenExpanded(emit, true);
} else {
emit(state.copyWith(isExpanded: e.isExpanded));
}
await _setViewIsExpanded(view, e.isExpanded);
},
viewDidUpdate: (e) {
e.result.fold(
(view) => emit(
state.copyWith(view: view, successOrFailure: left(unit)),
state.copyWith(
view: view,
childViews: view.childViews,
successOrFailure: left(unit),
),
),
(error) => emit(
state.copyWith(successOrFailure: right(error)),
@ -71,6 +89,36 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
),
);
},
move: (value) async {
final result = await ViewBackendService.moveViewV2(
viewId: value.from.id,
newParentId: value.newParentId,
prevViewId: value.prevId,
);
emit(
result.fold(
(l) => state.copyWith(successOrFailure: left(unit)),
(error) => state.copyWith(successOrFailure: right(error)),
),
);
},
createView: (e) async {
final result = await ViewBackendService.createView(
parentViewId: view.id,
name: e.name,
desc: '',
layoutType: e.layoutType,
initialDataBytes: null,
ext: {},
openAfterCreate: e.openAfterCreated,
);
emit(
result.fold(
(l) => state.copyWith(successOrFailure: left(unit)),
(error) => state.copyWith(successOrFailure: right(error)),
),
);
},
);
});
}
@ -80,15 +128,84 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
await listener.stop();
return super.close();
}
Future<void> _loadViewsWhenExpanded(
Emitter<ViewState> emit,
bool isExpanded,
) async {
if (!isExpanded) {
return;
}
if (state.childViews.isNotEmpty) {
// notify the old child views
emit(
state.copyWith(
childViews: state.childViews,
isExpanded: true,
),
);
}
final viewsOrFailed =
await ViewBackendService.getChildViews(viewId: state.view.id);
viewsOrFailed.fold(
(childViews) => emit(
state.copyWith(
childViews: childViews,
isExpanded: true,
),
),
(error) => emit(
state.copyWith(
successOrFailure: right(error),
isExpanded: true,
),
),
);
}
Future<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async {
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
final map = result.fold(
(l) => {},
(r) => jsonDecode(r),
);
if (isExpanded) {
map[view.id] = true;
} else {
map.remove(view.id);
}
await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));
}
Future<bool> _getViewIsExpanded(ViewPB view) {
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
return result.fold((l) => false, (r) {
final map = jsonDecode(r);
return map[view.id] ?? false;
});
});
}
}
@freezed
class ViewEvent with _$ViewEvent {
const factory ViewEvent.initial() = Initial;
const factory ViewEvent.setIsEditing(bool isEditing) = SetEditing;
const factory ViewEvent.setIsExpanded(bool isExpanded) = SetIsExpanded;
const factory ViewEvent.rename(String newName) = Rename;
const factory ViewEvent.delete() = Delete;
const factory ViewEvent.duplicate() = Duplicate;
const factory ViewEvent.move(
ViewPB from,
String newParentId,
String? prevId,
) = Move;
const factory ViewEvent.createView(
String name,
ViewLayoutPB layoutType, {
/// open the view after created
@Default(true) bool openAfterCreated,
}) = CreateView;
const factory ViewEvent.viewDidUpdate(Either<ViewPB, FlowyError> result) =
ViewDidUpdate;
}
@ -97,12 +214,16 @@ class ViewEvent with _$ViewEvent {
class ViewState with _$ViewState {
const factory ViewState({
required ViewPB view,
required List<ViewPB> childViews,
required bool isEditing,
required bool isExpanded,
required Either<Unit, FlowyError> successOrFailure,
}) = _ViewState;
factory ViewState.init(ViewPB view) => ViewState(
view: view,
childViews: view.childViews,
isExpanded: false,
isEditing: false,
successOrFailure: left(unit),
);

View File

@ -40,11 +40,23 @@ extension FlowyPluginExtension on FlowyPlugin {
extension ViewExtension on ViewPB {
Widget renderThumbnail({Color? iconColor}) {
const String thumbnail = "file_icon";
const Widget widget = FlowySvg(name: thumbnail);
return widget;
}
Widget icon() {
final iconName = switch (layout) {
ViewLayoutPB.Board => 'editor/board',
ViewLayoutPB.Calendar => 'editor/calendar',
ViewLayoutPB.Grid => 'editor/grid',
ViewLayoutPB.Document => 'editor/documents',
_ => 'file_icon',
};
return FlowySvg(
name: iconName,
);
}
PluginType get pluginType {
switch (layout) {
case ViewLayoutPB.Board:

View File

@ -141,6 +141,7 @@ class ViewBackendService {
return FolderEventUpdateView(payload).send();
}
// deprecated
static Future<Either<Unit, FlowyError>> moveView({
required String viewId,
required int fromIndex,
@ -154,6 +155,24 @@ class ViewBackendService {
return FolderEventMoveView(payload).send();
}
/// Move the view to the new parent view.
///
/// supports nested view
/// if the [prevViewId] is null, the view will be moved to the beginning of the list
static Future<Either<Unit, FlowyError>> moveViewV2({
required String viewId,
required String newParentId,
required String? prevViewId,
}) {
final payload = MoveNestedViewPayloadPB(
viewId: viewId,
newParentId: newParentId,
prevViewId: prevViewId,
);
return FolderEventMoveNestedView(payload).send();
}
Future<List<(ViewPB, List<ViewPB>)>> fetchViewsWithLayoutType(
ViewLayoutPB? layoutType,
) async {

View File

@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
import 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';
import 'package:appflowy_backend/log.dart';
@ -23,7 +24,6 @@ import 'package:styled_widget/styled_widget.dart';
import '../widgets/edit_panel/edit_panel.dart';
import 'home_layout.dart';
import 'home_stack.dart';
import 'menu/menu.dart';
class HomeScreen extends StatefulWidget {
final UserProfilePB user;
@ -118,7 +118,7 @@ class _HomeScreenState extends State<HomeScreen> {
buildContext: context,
),
);
final menu = _buildHomeMenu(
final menu = _buildHomeSidebar(
layout: layout,
context: context,
);
@ -140,16 +140,15 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
Widget _buildHomeMenu({
Widget _buildHomeSidebar({
required HomeLayout layout,
required BuildContext context,
}) {
final workspaceSetting = widget.workspaceSetting;
final homeMenu = HomeMenu(
final homeMenu = HomeSideBar(
user: widget.user,
workspaceSetting: workspaceSetting,
);
return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
}

View File

@ -83,6 +83,7 @@ class ImportPanel extends StatelessWidget {
e.toString(),
fontSize: 15,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.tertiary,
),
onTap: () async {
await _importFile(parentViewId, e);
@ -157,6 +158,8 @@ class ImportPanel extends StatelessWidget {
assert(false, 'Unsupported Type $importType');
}
}
importCallback(importType, '', null);
}
}

View File

@ -43,6 +43,7 @@ enum ImportType {
}
return FlowySvg(
name: name,
color: Theme.of(context).colorScheme.tertiary,
);
};

View File

@ -3,13 +3,12 @@ import 'package:appflowy/workspace/application/app/app_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:reorderables/reorderables.dart';
import 'item.dart';
class ViewSection extends StatelessWidget {
final ViewDataContext appViewData;
const ViewSection({Key? key, required this.appViewData}) : super(key: key);

View File

@ -0,0 +1,114 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalFolder extends StatefulWidget {
const PersonalFolder({
super.key,
required this.views,
});
final List<ViewPB> views;
@override
State<PersonalFolder> createState() => _PersonalFolderState();
}
class _PersonalFolderState extends State<PersonalFolder> {
bool isExpanded = true;
@override
Widget build(BuildContext context) {
return Column(
children: [
PersonalFolderHeader(
onPressed: () => setState(
() => isExpanded = !isExpanded,
),
onAdded: () => setState(() => isExpanded = true),
),
if (isExpanded)
...widget.views.map(
(view) => ViewItem(
key: ValueKey(view.id),
isFirstChild: view.id == widget.views.first.id,
view: view,
level: 0,
onSelected: (view) {
getIt<MenuSharedState>().latestOpenView = view;
context.read<MenuBloc>().add(MenuEvent.openPage(view.plugin()));
},
),
)
],
);
}
}
class PersonalFolderHeader extends StatefulWidget {
const PersonalFolderHeader({
super.key,
required this.onPressed,
required this.onAdded,
});
final VoidCallback onPressed;
final VoidCallback onAdded;
@override
State<PersonalFolderHeader> createState() => _PersonalFolderHeaderState();
}
class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
bool onHover = false;
@override
Widget build(BuildContext context) {
const iconSize = 26.0;
return MouseRegion(
onEnter: (event) => setState(() => onHover = true),
onExit: (event) => setState(() => onHover = false),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowyTextButton(
LocaleKeys.sideBar_personal.tr(),
tooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(),
constraints: const BoxConstraints(maxHeight: iconSize),
padding: const EdgeInsets.all(4),
fillColor: Colors.transparent,
onPressed: widget.onPressed,
),
if (onHover) ...[
const Spacer(),
FlowyIconButton(
tooltipText: LocaleKeys.sideBar_addAPage.tr(),
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
iconPadding: const EdgeInsets.all(2),
height: iconSize,
width: iconSize,
icon: const FlowySvg(name: 'editor/add'),
onPressed: () {
context.read<MenuBloc>().add(
MenuEvent.createApp(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
),
);
widget.onAdded();
},
),
]
],
),
);
}
}

View File

@ -0,0 +1,94 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Home Sidebar is the left side bar of the home page.
///
/// in the sidebar, we have:
/// - user icon, user name
/// - settings
/// - scrollable document list
/// - trash
class HomeSideBar extends StatelessWidget {
const HomeSideBar({
super.key,
required this.user,
required this.workspaceSetting,
});
final UserProfilePB user;
final WorkspaceSettingPB workspaceSetting;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => MenuBloc(
user: user,
workspace: workspaceSetting.workspace,
)..add(const MenuEvent.initial()),
child: BlocConsumer<MenuBloc, MenuState>(
builder: (context, state) => _buildSidebar(context, state),
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
listener: (context, state) => getIt<TabsBloc>().add(
TabsEvent.openPlugin(plugin: state.plugin),
),
),
);
}
Widget _buildSidebar(BuildContext context, MenuState state) {
final views = state.views;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
border: Border(
right: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// top menu
const SidebarTopMenu(),
// user, setting
SidebarUser(user: user),
// Favorite, Not supported yet
const VSpace(20),
// scrollable document list
Expanded(
child: SingleChildScrollView(
child: SidebarFolder(
views: views,
),
),
),
const VSpace(10),
// trash
const SidebarTrashButton(),
const VSpace(10),
// new page button
const Padding(
padding: EdgeInsets.only(left: 6.0),
child: SidebarNewPageButton(),
),
],
),
),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
class SidebarFolder extends StatelessWidget {
const SidebarFolder({
super.key,
required this.views,
});
final List<ViewPB> views;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// personal
PersonalFolder(views: views),
],
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarNewPageButton extends StatelessWidget {
const SidebarNewPageButton({
super.key,
});
@override
Widget build(BuildContext context) {
final child = FlowyTextButton(
LocaleKeys.newPageText.tr(),
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
fontColor: Theme.of(context).colorScheme.tertiary,
onPressed: () async => await _showCreatePageDialog(context),
heading: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: svgWidget('home/new_app'),
),
padding: const EdgeInsets.all(0),
);
return SizedBox(
height: 60,
child: TopBorder(
color: Theme.of(context).dividerColor,
child: child,
),
);
}
Future<void> _showCreatePageDialog(BuildContext context) async {
return NavigatorTextFieldDialog(
title: LocaleKeys.newPageText.tr(),
value: '',
confirm: (value) {
if (value.isNotEmpty) {
context.read<MenuBloc>().add(MenuEvent.createApp(value, desc: ''));
}
},
).show(context);
}
}

View File

@ -0,0 +1,85 @@
import 'dart:io' show Platform;
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Sidebar top menu is the top bar of the sidebar.
///
/// in the top menu, we have:
/// - appflowy icon (Windows or Linux)
/// - close / expand sidebar button
class SidebarTopMenu extends StatelessWidget {
const SidebarTopMenu({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<MenuBloc, MenuState>(
builder: (context, state) {
return SizedBox(
height: HomeSizes.topBarHeight,
child: MoveWindowDetector(
child: Row(
children: [
_buildLogoIcon(context),
const Spacer(),
_buildCollapseMenuButton(context),
],
),
),
);
},
);
}
Widget _buildLogoIcon(BuildContext context) {
if (Platform.isMacOS) {
return const SizedBox.shrink();
}
final name = Theme.of(context).brightness == Brightness.dark
? 'flowy_logo_dark_mode'
: 'flowy_logo_with_text';
return svgWidget(
name,
size: const Size(92, 17),
);
}
Widget _buildCollapseMenuButton(BuildContext context) {
final textSpan = TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n',
),
TextSpan(
// TODO(Lucas.Xu): it doesn't work on macOS.
text: Platform.isMacOS ? '⌘+\\' : 'Ctrl+\\',
),
],
);
return Tooltip(
richMessage: textSpan,
child: FlowyIconButton(
width: 28,
hoverColor: Colors.transparent,
onPressed: () => context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: const FlowySvg(
name: 'home/hide_menu',
),
),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
class SidebarTrashButton extends StatelessWidget {
const SidebarTrashButton({
super.key,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: getIt<MenuSharedState>().notifier,
builder: (context, value, child) {
return FlowyHover(
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).greySelect,
),
isSelected: () => getIt<MenuSharedState>().latestOpenView == null,
child: SizedBox(
height: 26,
child: InkWell(
onTap: () {
getIt<MenuSharedState>().latestOpenView = null;
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: makePlugin(pluginType: PluginType.trash),
),
);
},
child: _buildTextButton(context),
),
),
);
},
);
}
Widget _buildTextButton(BuildContext context) {
return Row(
children: [
const HSpace(6),
const FlowySvg(
size: Size(16, 16),
name: 'home/trash',
),
const HSpace(6),
FlowyText.medium(
LocaleKeys.trash_text.tr(),
),
],
);
}
}

View File

@ -0,0 +1,149 @@
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
class SidebarUser extends StatelessWidget {
const SidebarUser({
super.key,
required this.user,
});
final UserProfilePB user;
@override
Widget build(BuildContext context) {
return BlocProvider<MenuUserBloc>(
create: (context) => getIt<MenuUserBloc>(param1: user)
..add(
const MenuUserEvent.initial(),
),
child: BlocBuilder<MenuUserBloc, MenuUserState>(
builder: (context, state) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildAvatar(context),
const HSpace(10),
Expanded(
child: _buildUserName(context),
),
_buildSettingsButton(context),
],
),
),
);
}
Widget _buildAvatar(BuildContext context) {
String iconUrl = context.read<MenuUserBloc>().state.userProfile.iconUrl;
if (iconUrl.isEmpty) {
iconUrl = defaultUserAvatar;
final String name = _userName(
context.read<MenuUserBloc>().state.userProfile,
);
final Color color = ColorGenerator().generateColorFromString(name);
const initialsCount = 2;
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = name
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join('');
return Container(
width: 28,
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: FlowyText.semibold(
nameInitials,
color: Colors.white,
fontSize: nameInitials.length == initialsCount ? 12 : 14,
),
);
}
return SizedBox.square(
dimension: 25,
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: svgWidget('emoji/$iconUrl'),
),
),
);
}
Widget _buildUserName(BuildContext context) {
final String name = _userName(
context.read<MenuUserBloc>().state.userProfile,
);
return FlowyText.medium(
name,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.tertiary,
);
}
Widget _buildSettingsButton(BuildContext context) {
final userProfile = context.read<MenuUserBloc>().state.userProfile;
return Tooltip(
message: LocaleKeys.settings_menu_open.tr(),
child: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return BlocProvider<DocumentAppearanceCubit>.value(
value: BlocProvider.of<DocumentAppearanceCubit>(context),
child: SettingsDialog(
userProfile,
didLogout: () async {
Navigator.of(context).pop();
await FlowyRunner.run(
FlowyApp(),
integrationEnv(),
);
},
dismissDialog: () => Navigator.of(context).pop(),
),
);
},
);
},
icon: SizedBox.square(
dimension: 20,
child: svgWidget(
'home/settings',
color: Theme.of(context).colorScheme.tertiary,
),
),
),
);
}
/// Return the user name, if the user name is empty, return the default user name.
String _userName(UserProfilePB userProfile) {
String name = userProfile.name;
if (name.isEmpty) {
name = LocaleKeys.defaultUsername.tr();
}
return name;
}
}

View File

@ -0,0 +1,172 @@
import 'package:appflowy/workspace/application/view/view_bloc.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';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
enum DraggableHoverPosition {
none,
top,
center,
bottom,
}
class DraggableViewItem extends StatefulWidget {
const DraggableViewItem({
super.key,
required this.view,
this.feedback,
required this.child,
this.isFirstChild = false,
});
final Widget child;
final WidgetBuilder? feedback;
final ViewPB view;
final bool isFirstChild;
@override
State<DraggableViewItem> createState() => _DraggableViewItemState();
}
class _DraggableViewItemState extends State<DraggableViewItem> {
DraggableHoverPosition position = DraggableHoverPosition.none;
@override
Widget build(BuildContext context) {
// add top border if the draggable item is on the top of the list
// highlight the draggable item if the draggable item is on the center
// add bottom border if the draggable item is on the bottom of the list
final child = Column(
mainAxisSize: MainAxisSize.min,
children: [
// only show the top border when the draggable item is the first child
if (widget.isFirstChild)
Divider(
height: 2,
thickness: 2,
color: position == DraggableHoverPosition.top
? Theme.of(context).colorScheme.secondary
: Colors.transparent,
),
Container(
color: position == DraggableHoverPosition.center
? Theme.of(context).colorScheme.secondary.withOpacity(0.5)
: Colors.transparent,
child: widget.child,
),
Divider(
height: 2,
thickness: 2,
color: position == DraggableHoverPosition.bottom
? Theme.of(context).colorScheme.secondary
: Colors.transparent,
),
],
);
return DraggableItem<ViewPB>(
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);
setState(() {
position = _computeHoverPosition(offset, renderBox.size);
Log.debug(
'offset: $offset, position: $position, size: ${renderBox.size}',
);
});
},
onLeave: (_) => setState(
() => position = DraggableHoverPosition.none,
),
onAccept: (data) {
_move(data, widget.view);
setState(
() => position = DraggableHoverPosition.none,
);
},
feedback: IntrinsicWidth(
child: Opacity(
opacity: 0.5,
child: widget.feedback?.call(context) ?? child,
),
),
child: child,
);
}
void _move(ViewPB from, ViewPB to) {
switch (position) {
case DraggableHoverPosition.top:
context.read<ViewBloc>().add(
ViewEvent.move(
from,
to.parentViewId,
null,
),
);
break;
case DraggableHoverPosition.bottom:
context.read<ViewBloc>().add(
ViewEvent.move(
from,
to.parentViewId,
to.id,
),
);
break;
case DraggableHoverPosition.center:
context.read<ViewBloc>().add(
ViewEvent.move(
from,
to.id,
to.childViews.lastOrNull?.id,
),
);
break;
case DraggableHoverPosition.none:
break;
}
}
DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) {
final threshold = size.height / 4.0;
if (widget.isFirstChild && offset.dy < -5.0) {
return DraggableHoverPosition.top;
}
if (offset.dy > threshold) {
return DraggableHoverPosition.bottom;
}
return DraggableHoverPosition.center;
}
bool _shouldAccept(ViewPB data) {
// ignore moving the view to itself
if (data.id == widget.view.id) {
return false;
}
// ignore moving the view to its child view
if (data.containsView(widget.view)) {
return false;
}
return true;
}
}
extension on ViewPB {
bool containsView(ViewPB view) {
if (id == view.id) {
return true;
}
return childViews.any((v) => v.containsView(view));
}
}

View File

@ -0,0 +1,54 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flutter/material.dart';
enum ViewMoreActionType {
delete,
addToFavorites, // not supported yet.
duplicate,
copyLink, // not supported yet.
rename,
moveTo, // not supported yet.
openInNewTab,
}
extension ViewMoreActionTypeExtension on ViewMoreActionType {
String get name {
switch (this) {
case ViewMoreActionType.delete:
return LocaleKeys.disclosureAction_delete.tr();
case ViewMoreActionType.addToFavorites:
return LocaleKeys.disclosureAction_addToFavorites.tr();
case ViewMoreActionType.duplicate:
return LocaleKeys.disclosureAction_duplicate.tr();
case ViewMoreActionType.copyLink:
return LocaleKeys.disclosureAction_copyLink.tr();
case ViewMoreActionType.rename:
return LocaleKeys.disclosureAction_rename.tr();
case ViewMoreActionType.moveTo:
return LocaleKeys.disclosureAction_moveTo.tr();
case ViewMoreActionType.openInNewTab:
return LocaleKeys.disclosureAction_openNewTab.tr();
}
}
Widget icon(Color iconColor) {
switch (this) {
case ViewMoreActionType.delete:
return const FlowySvg(name: 'editor/delete');
case ViewMoreActionType.addToFavorites:
return const Icon(Icons.favorite);
case ViewMoreActionType.duplicate:
return const FlowySvg(name: 'editor/copy');
case ViewMoreActionType.copyLink:
return const Icon(Icons.copy);
case ViewMoreActionType.rename:
return const FlowySvg(name: 'editor/edit');
case ViewMoreActionType.moveTo:
return const Icon(Icons.move_to_inbox);
case ViewMoreActionType.openInNewTab:
return const FlowySvg(name: 'grid/expander');
}
}
}

View File

@ -0,0 +1,130 @@
import 'package:appflowy/plugins/document/document.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/header/import/import_panel.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
class ViewAddButton extends StatelessWidget {
const ViewAddButton({
super.key,
required this.parentViewId,
required this.onEditing,
required this.onSelected,
});
final String parentViewId;
final void Function(bool value) onEditing;
final Function(
PluginBuilder,
String? name,
List<int>? initialDataBytes,
bool openAfterCreated,
bool createNewView,
) onSelected;
List<PopoverAction> get _actions {
return [
// document, grid, kanban, calendar
...pluginBuilders().map(
(pluginBuilder) => ViewAddButtonActionWrapper(
pluginBuilder: pluginBuilder,
),
),
// import from ...
...getIt<PluginSandbox>().builders.whereType<DocumentPluginBuilder>().map(
(pluginBuilder) => ViewImportActionWrapper(
pluginBuilder: pluginBuilder,
),
),
];
}
@override
Widget build(BuildContext context) {
return PopoverActionList<PopoverAction>(
direction: PopoverDirection.bottomWithLeftAligned,
actions: _actions,
offset: const Offset(0, 8),
buildChild: (popover) {
return FlowyIconButton(
hoverColor: Colors.transparent,
iconPadding: const EdgeInsets.all(2),
width: 26,
icon: const FlowySvg(name: 'editor/add'),
onPressed: () {
onEditing(true);
popover.show();
},
);
},
onSelected: (action, popover) {
onEditing(false);
if (action is ViewAddButtonActionWrapper) {
_showViewAddButtonActions(context, action);
} else if (action is ViewImportActionWrapper) {
_showViewImportAction(context, action);
}
popover.close();
},
onClosed: () {
onEditing(false);
},
);
}
void _showViewAddButtonActions(
BuildContext context,
ViewAddButtonActionWrapper action,
) {
onSelected(action.pluginBuilder, null, null, true, true);
}
void _showViewImportAction(
BuildContext context,
ViewImportActionWrapper action,
) {
showImportPanel(
parentViewId,
context,
(type, name, initialDataBytes) {
onSelected(action.pluginBuilder, null, null, true, false);
},
);
}
}
class ViewAddButtonActionWrapper extends ActionCell {
ViewAddButtonActionWrapper({
required this.pluginBuilder,
});
final PluginBuilder pluginBuilder;
@override
Widget? leftIcon(Color iconColor) => FlowySvg(name: pluginBuilder.menuIcon);
@override
String get name => pluginBuilder.menuName;
PluginType get pluginType => pluginBuilder.pluginType;
}
class ViewImportActionWrapper extends ActionCell {
ViewImportActionWrapper({
required this.pluginBuilder,
});
final DocumentPluginBuilder pluginBuilder;
@override
Widget? leftIcon(Color iconColor) => const FlowySvg(name: 'editor/import');
@override
String get name => LocaleKeys.moreAction_import.tr();
}

View File

@ -0,0 +1,322 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ViewItem extends StatelessWidget {
const ViewItem({
super.key,
required this.view,
required this.level,
this.leftPadding = 10,
required this.onSelected,
this.isFirstChild = false,
this.isDraggable = true,
});
final ViewPB view;
// indicate the level of the view item
// used to calculate the left padding
final int level;
// the left padding of the view item for each level
// the left padding of the each level = level * leftPadding
final double leftPadding;
final void Function(ViewPB) onSelected;
// used for indicating the first child of the parent view, so that we can
// add top border to the first child
final bool isFirstChild;
// it should be false when it's rendered as feedback widget inside DraggableItem
final bool isDraggable;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
view.childViews
..clear()
..addAll(state.childViews);
return InnerViewItem(
view: view,
level: level,
leftPadding: leftPadding,
showActions: state.isEditing,
isExpanded: state.isExpanded,
onSelected: onSelected,
isFirstChild: isFirstChild,
isDraggable: isDraggable,
);
},
),
);
}
}
class InnerViewItem extends StatelessWidget {
const InnerViewItem({
super.key,
required this.view,
this.isDraggable = true,
this.isExpanded = true,
required this.level,
this.leftPadding = 10,
required this.showActions,
required this.onSelected,
this.isFirstChild = false,
});
final ViewPB view;
final bool isDraggable;
final bool isExpanded;
final bool isFirstChild;
final int level;
final double leftPadding;
final bool showActions;
final void Function(ViewPB) onSelected;
@override
Widget build(BuildContext context) {
Widget child = SingleInnerViewItem(
view: view,
level: level,
showActions: showActions,
onSelected: onSelected,
isExpanded: isExpanded,
);
// if the view is expanded and has child views, render its child views
final childViews = view.childViews;
if (isExpanded && childViews.isNotEmpty) {
final children = childViews.map((childView) {
return ViewItem(
key: ValueKey(childView.id),
isFirstChild: childView.id == childViews.first.id,
view: childView,
level: level + 1,
onSelected: onSelected,
isDraggable: isDraggable,
);
}).toList();
child = Column(
mainAxisSize: MainAxisSize.min,
children: [
child,
...children,
],
);
}
// wrap the child with DraggableItem if isDraggable is true
if (isDraggable) {
child = DraggableViewItem(
isFirstChild: isFirstChild,
view: view,
child: child,
feedback: (context) {
return ViewItem(
view: view,
level: level,
onSelected: onSelected,
isDraggable: false,
);
},
);
}
return child;
}
}
class SingleInnerViewItem extends StatefulWidget {
const SingleInnerViewItem({
super.key,
required this.view,
required this.isExpanded,
required this.level,
this.leftPadding = 10,
required this.showActions,
required this.onSelected,
});
final ViewPB view;
final bool isExpanded;
final int level;
final double leftPadding;
final bool showActions;
final void Function(ViewPB) onSelected;
@override
State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();
}
class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
@override
Widget build(BuildContext context) {
return FlowyHover(
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.secondary,
),
buildWhenOnHover: () => !widget.showActions,
builder: (_, onHover) => _buildViewItem(onHover),
isSelected: () =>
widget.showActions ||
getIt<MenuSharedState>().latestOpenView?.id == widget.view.id,
);
}
Widget _buildViewItem(bool onHover) {
final children = [
// expand icon
_buildExpandedIcon(),
const HSpace(7),
// icon
SizedBox.square(
dimension: 16,
child: widget.view.icon(),
),
const HSpace(5),
// title
Expanded(
child: FlowyText.regular(
widget.view.name,
overflow: TextOverflow.ellipsis,
),
)
];
// hover action
if (widget.showActions || onHover) {
// ··· more action button
children.add(_buildViewMoreActionButton(context));
// + button
children.add(_buildViewAddButton(context));
}
return GestureDetector(
onTap: () => widget.onSelected(widget.view),
child: SizedBox(
height: 26,
child: Padding(
padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
child: Row(
children: children,
),
),
),
);
}
// > button
Widget _buildExpandedIcon() {
final name =
widget.isExpanded ? 'home/drop_down_show' : 'home/drop_down_hide';
return GestureDetector(
child: FlowySvg(
name: name,
size: const Size.square(16.0),
),
onTap: () => context
.read<ViewBloc>()
.add(ViewEvent.setIsExpanded(!widget.isExpanded)),
);
}
// + button
Widget _buildViewAddButton(BuildContext context) {
return Tooltip(
message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),
child: ViewAddButton(
parentViewId: widget.view.id,
onEditing: (value) =>
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
onSelected: (
pluginBuilder,
name,
initialDataBytes,
openAfterCreated,
createNewView,
) {
if (createNewView) {
context.read<ViewBloc>().add(
ViewEvent.createView(
name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
pluginBuilder.layoutType!,
openAfterCreated: openAfterCreated,
),
);
}
context.read<ViewBloc>().add(
const ViewEvent.setIsExpanded(true),
);
},
),
);
}
// ··· more action button
Widget _buildViewMoreActionButton(BuildContext context) {
return Tooltip(
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
child: ViewMoreActionButton(
onEditing: (value) =>
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
onAction: (action) {
switch (action) {
case ViewMoreActionType.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.disclosureAction_rename.tr(),
autoSelectAllText: true,
value: widget.view.name,
confirm: (newValue) {
context.read<ViewBloc>().add(ViewEvent.rename(newValue));
},
).show(context);
break;
case ViewMoreActionType.delete:
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case ViewMoreActionType.duplicate:
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case ViewMoreActionType.openInNewTab:
context.read<TabsBloc>().add(
TabsEvent.openTab(
plugin: widget.view.plugin(),
view: widget.view,
),
);
break;
default:
throw UnsupportedError('$action is not supported');
}
},
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra/image.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
const supportedActionTypes = [
ViewMoreActionType.rename,
ViewMoreActionType.delete,
ViewMoreActionType.duplicate,
ViewMoreActionType.openInNewTab,
];
/// ··· button beside the view name
class ViewMoreActionButton extends StatelessWidget {
const ViewMoreActionButton({
super.key,
required this.onEditing,
required this.onAction,
});
final void Function(bool value) onEditing;
final void Function(ViewMoreActionType) onAction;
@override
Widget build(BuildContext context) {
return PopoverActionList<ViewMoreActionTypeWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
offset: const Offset(0, 8),
actions: supportedActionTypes
.map((e) => ViewMoreActionTypeWrapper(e))
.toList(),
buildChild: (popover) {
return FlowyIconButton(
hoverColor: Colors.transparent,
iconPadding: const EdgeInsets.all(2),
width: 26,
icon: const FlowySvg(name: 'editor/details'),
onPressed: () {
onEditing(true);
popover.show();
},
);
},
onSelected: (action, popover) {
onEditing(false);
onAction(action.inner);
popover.close();
},
onClosed: () => onEditing(false),
);
}
}
class ViewMoreActionTypeWrapper extends ActionCell {
ViewMoreActionTypeWrapper(this.inner);
final ViewMoreActionType inner;
@override
Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
@override
String get name => inner.name;
}

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
class DraggableItem<T extends Object> extends StatefulWidget {
const DraggableItem({
super.key,
required this.child,
required this.data,
this.feedback,
this.childWhenDragging,
this.onAccept,
this.onWillAccept,
this.onMove,
this.onLeave,
this.enableAutoScroll = true,
this.hitTestSize = const Size(100, 100),
});
final T data;
final Widget child;
final Widget? feedback;
final Widget? childWhenDragging;
final DragTargetAccept<T>? onAccept;
final DragTargetWillAccept<T>? onWillAccept;
final DragTargetMove<T>? onMove;
final DragTargetLeave<T>? onLeave;
/// Whether to enable auto scroll when dragging.
///
/// If true, the draggable item must be wrapped inside a [Scrollable] widget.
final bool enableAutoScroll;
final Size hitTestSize;
@override
State<DraggableItem<T>> createState() => _DraggableItemState<T>();
}
class _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {
ScrollableState? scrollable;
EdgeDraggingAutoScroller? autoScroller;
Rect? dragTarget;
@override
void didChangeDependencies() {
super.didChangeDependencies();
initAutoScrollerIfNeeded(context);
}
@override
Widget build(BuildContext context) {
initAutoScrollerIfNeeded(context);
return DragTarget(
onAccept: widget.onAccept,
onWillAccept: widget.onWillAccept,
onMove: widget.onMove,
onLeave: widget.onLeave,
builder: (_, __, ___) => Draggable<T>(
data: widget.data,
feedback: widget.feedback ?? widget.child,
childWhenDragging: widget.childWhenDragging ?? widget.child,
child: widget.child,
onDragUpdate: (details) {
if (widget.enableAutoScroll) {
dragTarget = details.globalPosition & widget.hitTestSize;
autoScroller?.startAutoScrollIfNecessary(dragTarget!);
}
},
onDragEnd: (details) {
autoScroller?.stopAutoScroll();
dragTarget = null;
},
onDraggableCanceled: (_, __) {
autoScroller?.stopAutoScroll();
dragTarget = null;
},
),
);
}
void initAutoScrollerIfNeeded(BuildContext context) {
if (!widget.enableAutoScroll) {
return;
}
scrollable = Scrollable.of(context);
if (scrollable == null) {
throw FlutterError(
'DraggableItem must be wrapped inside a Scrollable widget '
'when enableAutoScroll is true.',
);
}
autoScroller?.stopAutoScroll();
autoScroller = EdgeDraggingAutoScroller(
scrollable!,
onScrollViewScrolled: () {
if (dragTarget != null) {
autoScroller!.startAutoScrollIfNecessary(dragTarget!);
}
},
velocityScalar: 20,
);
}
}

View File

@ -24,3 +24,28 @@ extension FlowyStyledWidget on Widget {
);
}
}
class TopBorder extends StatelessWidget {
const TopBorder({
super.key,
this.width = 1.0,
this.color = Colors.grey,
required this.child,
});
final Widget child;
final double width;
final Color color;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border(
top: BorderSide(width: width, color: color),
),
),
child: child,
);
}
}

View File

@ -69,4 +69,32 @@ void main() {
assert(appBloc.state.views.isEmpty);
});
test('create nested view test', () async {
final app = await testContext.createTestApp();
final appBloc = AppBloc(view: app);
appBloc
..add(
const AppEvent.initial(),
)
..add(
const AppEvent.createView('Document 1', ViewLayoutPB.Document),
);
await blocResponseFuture();
// create a nested view
const name = 'Document 1 - 1';
final viewBloc = ViewBloc(view: appBloc.state.views.first);
viewBloc
..add(
const ViewEvent.initial(),
)
..add(
const ViewEvent.createView(name, ViewLayoutPB.Document),
);
await blocResponseFuture();
assert(viewBloc.state.childViews.first.name == name);
});
}

View File

@ -70,7 +70,10 @@
"rename": "Rename",
"delete": "Delete",
"duplicate": "Duplicate",
"openNewTab": "Open in a new tab"
"openNewTab": "Open in a new tab",
"moveTo": "Move to",
"addToFavorites": "Add to Favorites",
"copyLink": "Copy Link"
},
"blankPageTitle": "Blank page",
"newPageText": "New page",
@ -111,6 +114,7 @@
"feedback": "Feedback"
},
"menuAppHeader": {
"moreButtonToolTip": "Remove, rename, and more...",
"addPageTooltip": "Quickly add a page inside",
"defaultNewPageName": "Untitled",
"renameDialog": "Rename"
@ -146,7 +150,11 @@
},
"sideBar": {
"closeSidebar": "Close side bar",
"openSidebar": "Open side bar"
"openSidebar": "Open side bar",
"personal": "Personal",
"favorites": "Favorites",
"clickToHidePersonal": "Click to hide personal section",
"addAPage": "Add a page"
},
"notifications": {
"export": {

View File

@ -178,9 +178,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcdcf0d683fe9c23d32cf5b53c9918ea0a500375a9fb20109802552658e576c9"
checksum = "fc00553f5f3c06ffd4510a9d576f92143618706c45ea6ff81e84ad9be9588abd"
dependencies = [
"aws-credential-types",
"aws-http",
@ -208,9 +208,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcdb2f7acbc076ff5ad05e7864bdb191ca70a6fd07668dc3a1a8bcd051de5ae"
checksum = "4cb57ac6088805821f78d282c0ba8aec809f11cbee10dda19a97b03ab040ccc2"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@ -222,9 +222,9 @@ dependencies = [
[[package]]
name = "aws-endpoint"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cce1c41a6cfaa726adee9ebb9a56fcd2bbfd8be49fd8a04c5e20fd968330b04"
checksum = "9c5f6f84a4f46f95a9bb71d9300b73cd67eb868bc43ae84f66ad34752299f4ac"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@ -236,9 +236,9 @@ dependencies = [
[[package]]
name = "aws-http"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aadbc44e7a8f3e71c8b374e03ecd972869eb91dd2bc89ed018954a52ba84bc44"
checksum = "a754683c322f7dc5167484266489fdebdcd04d26e53c162cad1f3f949f2c5671"
dependencies = [
"aws-credential-types",
"aws-smithy-http",
@ -281,9 +281,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "0.28.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8b812340d86d4a766b2ca73f740dfd47a97c2dff0c06c8517a16d88241957e4"
checksum = "babfd626348836a31785775e3c08a4c345a5ab4c6e06dfd9167f2bee0e6295d6"
dependencies = [
"aws-credential-types",
"aws-endpoint",
@ -306,9 +306,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "0.28.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265fac131fbfc188e5c3d96652ea90ecc676a934e3174eaaee523c6cec040b3b"
checksum = "2d0fbe3c2c342bc8dfea4bb43937405a8ec06f99140a0dcb9c7b59e54dfa93a1"
dependencies = [
"aws-credential-types",
"aws-endpoint",
@ -332,9 +332,9 @@ dependencies = [
[[package]]
name = "aws-sig-auth"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b94acb10af0c879ecd5c7bdf51cda6679a0a4f4643ce630905a77673bfa3c61"
checksum = "84dc92a63ede3c2cbe43529cb87ffa58763520c96c6a46ca1ced80417afba845"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@ -346,9 +346,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c"
checksum = "392fefab9d6fcbd76d518eb3b1c040b84728ab50f58df0c3c53ada4bea9d327e"
dependencies = [
"aws-smithy-http",
"form_urlencoded",
@ -365,9 +365,9 @@ dependencies = [
[[package]]
name = "aws-smithy-async"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880"
checksum = "ae23b9fe7a07d0919000116c4c5c0578303fbce6fc8d32efca1f7759d4c20faf"
dependencies = [
"futures-util",
"pin-project-lite",
@ -377,9 +377,9 @@ dependencies = [
[[package]]
name = "aws-smithy-client"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a86aa6e21e86c4252ad6a0e3e74da9617295d8d6e374d552be7d3059c41cedd"
checksum = "5230d25d244a51339273b8870f0f77874cd4449fb4f8f629b21188ae10cfc0ba"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@ -401,9 +401,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28"
checksum = "b60e2133beb9fe6ffe0b70deca57aaeff0a35ad24a9c6fab2fd3b4f45b99fdb5"
dependencies = [
"aws-smithy-types",
"bytes",
@ -423,9 +423,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-tower"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9"
checksum = "3a4d94f556c86a0dd916a5d7c39747157ea8cb909ca469703e20fee33e448b67"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
@ -439,18 +439,18 @@ dependencies = [
[[package]]
name = "aws-smithy-json"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23f9f42fbfa96d095194a632fbac19f60077748eba536eb0b9fecc28659807f8"
checksum = "5ce3d6e6ebb00b2cce379f079ad5ec508f9bcc3a9510d9b9c1840ed1d6f8af39"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-query"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98819eb0b04020a1c791903533b638534ae6c12e2aceda3e6e6fba015608d51d"
checksum = "d58edfca32ef9bfbc1ca394599e17ea329cb52d6a07359827be74235b64b3298"
dependencies = [
"aws-smithy-types",
"urlencoding",
@ -458,9 +458,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8"
checksum = "58db46fc1f4f26be01ebdb821751b4e2482cd43aa2b64a0348fb89762defaffa"
dependencies = [
"base64-simd",
"itoa",
@ -471,18 +471,18 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1b9d12875731bd07e767be7baad95700c3137b56730ec9ddeedb52a5e5ca63b"
checksum = "fb557fe4995bd9ec87fb244bbb254666a971dc902a783e9da8b7711610e9664c"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "0.55.3"
version = "0.55.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd209616cc8d7bfb82f87811a5c655dc97537f592689b18743bddf5dc5c4829"
checksum = "de0869598bfe46ec44ffe17e063ed33336e59df90356ca8ff0e8da6f7c1d994b"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@ -3372,9 +3372,9 @@ dependencies = [
[[package]]
name = "postgrest"
version = "1.6.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7"
checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b"
dependencies = [
"reqwest",
]
@ -4030,9 +4030,9 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
@ -4151,15 +4151,15 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
version = "1.0.175"
version = "1.0.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348"
dependencies = [
"serde_derive",
]
@ -4177,9 +4177,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.175"
version = "1.0.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b"
dependencies = [
"proc-macro2",
"quote",
@ -5085,9 +5085,9 @@ dependencies = [
[[package]]
name = "urlencoding"
version = "2.1.3"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "utf-8"

View File

@ -100,23 +100,18 @@ impl FolderOperationHandler for DocumentFolderOperation {
FutureResult::new(async move {
let mut write_guard = workspace_view_builder.write().await;
// Create a parent view named "⭐️ Getting started". and a child view named "Read me".
// Create a view named "⭐️ Getting started" with built-in README data.
// Don't modify this code unless you know what you are doing.
write_guard
.with_view_builder(|view_builder| async {
view_builder
.with_name("⭐️ Getting started")
.with_child_view_builder(|child_view_builder| async {
let view = child_view_builder.with_name("Read me").build();
let json_str = include_str!("../../assets/read_me.json");
let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap();
manager
.create_document(&view.parent_view.id, Some(document_pb.into()))
.unwrap();
view
})
.await
.build()
let view = view_builder.with_name("⭐️ Getting started").build();
// create a empty document
let json_str = include_str!("../../assets/read_me.json");
let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap();
manager
.create_document(&view.parent_view.id, Some(document_pb.into()))
.unwrap();
view
})
.await;
Ok(())

View File

@ -27,14 +27,7 @@ impl DefaultFolderBuilder {
let views = workspace_view_builder.write().await.build();
// Safe to unwrap because we have at least one view. check out the DocumentFolderOperation.
let first_view = views
.first()
.unwrap()
.child_views
.first()
.unwrap()
.parent_view
.clone();
let first_view = views.first().unwrap().parent_view.clone();
let first_level_views = views
.iter()