Merge branch 'upstream-main' into feat/tauri-kanban

# Conflicts:
#	frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts
#	frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useDatabase.ts
#	frontend/appflowy_tauri/src/appflowy_app/components/board/Board.tsx
#	frontend/appflowy_tauri/src/appflowy_app/components/board/BoardBlock.tsx
This commit is contained in:
ascarbek 2023-03-15 11:34:21 +06:00
commit 6e028c4f64
44 changed files with 1461 additions and 345 deletions

View File

@ -118,11 +118,11 @@ jobs:
fi fi
shell: bash shell: bash
- uses: codecov/codecov-action@v3 # - uses: codecov/codecov-action@v3
with: # with:
name: appflowy # name: appflowy
flags: appflowy # flags: appflowy
env_vars: ${{ matrix.os }} # env_vars: ${{ matrix.os }}
fail_ci_if_error: true # fail_ci_if_error: true
verbose: true # verbose: true

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 15V6.9C3.5 6.05992 3.5 5.63988 3.66349 5.31901C3.8073 5.03677 4.03677 4.8073 4.31901 4.66349C4.63988 4.5 5.05992 4.5 5.9 4.5H8.47237C8.84808 4.5 9.03594 4.5 9.20646 4.55179C9.35741 4.59763 9.49785 4.6728 9.61972 4.77298C9.75739 4.88614 9.86159 5.04245 10.07 5.35507L10.93 6.64533C11.1384 6.95795 11.2426 7.11426 11.3803 7.22742C11.5022 7.3276 11.6426 7.40277 11.7935 7.44861C11.9641 7.5004 12.1519 7.5004 12.5276 7.5004H16.5004C16.965 7.5004 17.1973 7.5004 17.3879 7.55143C17.9058 7.69008 18.3103 8.09459 18.449 8.61248C18.5 8.80308 18.5 9.03539 18.5 9.5V9.5M10.5 13.5H16.5" stroke="#222222" stroke-linecap="round"/>
<path d="M4.5 18.5L16.7701 18.5004C17.3922 18.5004 17.7032 18.5004 17.9679 18.3963C18.2016 18.3044 18.4084 18.1553 18.5695 17.9626C18.752 17.7445 18.8503 17.4494 19.047 16.8593L20.4471 12.6592C20.8026 11.5927 20.9803 11.0595 20.8737 10.635C20.7804 10.2635 20.5485 9.94171 20.2255 9.7357C19.8566 9.50035 19.2945 9.50033 18.1703 9.5003L10.2299 9.50005C9.60784 9.50003 9.29681 9.50002 9.03216 9.60411C8.79846 9.69601 8.59157 9.84513 8.43047 10.0378C8.24804 10.2559 8.14968 10.551 7.95298 11.1411L5.7649 17.7057C5.60671 18.1803 5.16255 18.5004 4.66227 18.5004V18.5004C4.02037 18.5004 3.5 17.98 3.5 17.3381V14.5" stroke="#222222"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -344,23 +344,38 @@
"referencedGrid": "Referenced Grid", "referencedGrid": "Referenced Grid",
"autoCompletionMenuItemName": "Auto Completion", "autoCompletionMenuItemName": "Auto Completion",
"autoGeneratorMenuItemName": "Auto Generator", "autoGeneratorMenuItemName": "Auto Generator",
"autoGeneratorTitleName": "Open AI: Auto Generator", "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
"autoGeneratorLearnMore": "Learn more", "autoGeneratorLearnMore": "Learn more",
"autoGeneratorGenerate": "Generate", "autoGeneratorGenerate": "Generate",
"autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...", "autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
"autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key", "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
"smartEditTitleName": "Open AI: Smart Edit", "smartEdit": "Smart Edit",
"smartEditTitleName": "OpenAI: Smart Edit",
"smartEditFixSpelling": "Fix spelling", "smartEditFixSpelling": "Fix spelling",
"smartEditSummarize": "Summarize", "smartEditSummarize": "Summarize",
"smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
"smartEditCouldNotFetchKey": "Could not fetch OpenAI key", "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
"smartEditDisabled": "Connect OpenAI in Settings",
"cover": { "cover": {
"changeCover": "Change Cover", "changeCover": "Change Cover",
"colors": "Colors", "colors": "Colors",
"images": "Images", "images": "Images",
"abstract": "Abstract", "abstract": "Abstract",
"addCover": "Add Cover", "addCover": "Add Cover",
"addLocalImage": "Add local image" "addLocalImage": "Add local image",
"invalidImageUrl": "Invalid image URL",
"failedToAddImageToGallery": "Failed to add image to gallery",
"enterImageUrl": "Enter image URL",
"add": "Add",
"back": "Back",
"saveToGallery": "Save to gallery",
"removeIcon": "Remove Icon",
"pasteImageUrl": "Paste image URL",
"or": "OR",
"pickFromFiles": "Pick from files",
"couldNotFetchImage": "Could not fetch image",
"imageSavingFailed": "Image Saving Failed",
"addIcon": "Add Icon"
} }
} }
}, },

View File

@ -12,7 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'dart:async'; import 'dart:async';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'database_service.dart'; import 'database_view_service.dart';
import 'defines.dart'; import 'defines.dart';
import 'layout/layout_setting_listener.dart'; import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart'; import 'row/row_cache.dart';
@ -67,7 +67,7 @@ class DatabaseCallbacks {
class DatabaseController { class DatabaseController {
final String viewId; final String viewId;
final DatabaseBackendService _databaseBackendSvc; final DatabaseViewBackendService _databaseViewBackendSvc;
final FieldController fieldController; final FieldController fieldController;
late DatabaseViewCache _viewCache; late DatabaseViewCache _viewCache;
final LayoutTypePB layoutType; final LayoutTypePB layoutType;
@ -87,7 +87,7 @@ class DatabaseController {
DatabaseController({required ViewPB view, required this.layoutType}) DatabaseController({required ViewPB view, required this.layoutType})
: viewId = view.id, : viewId = view.id,
_databaseBackendSvc = DatabaseBackendService(viewId: view.id), _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id), fieldController = FieldController(viewId: view.id),
groupListener = DatabaseGroupListener(view.id), groupListener = DatabaseGroupListener(view.id),
layoutListener = DatabaseLayoutListener(view.id) { layoutListener = DatabaseLayoutListener(view.id) {
@ -112,7 +112,7 @@ class DatabaseController {
} }
Future<Either<Unit, FlowyError>> open() async { Future<Either<Unit, FlowyError>> open() async {
return _databaseBackendSvc.openGrid().then((result) { return _databaseViewBackendSvc.openGrid().then((result) {
return result.fold( return result.fold(
(database) async { (database) async {
_databaseCallbacks?.onDatabaseChanged?.call(database); _databaseCallbacks?.onDatabaseChanged?.call(database);
@ -152,7 +152,7 @@ class DatabaseController {
cellDataByFieldId = rowBuilder.build(); cellDataByFieldId = rowBuilder.build();
} }
return _databaseBackendSvc.createRow( return _databaseViewBackendSvc.createRow(
startRowId: startRowId, startRowId: startRowId,
groupId: groupId, groupId: groupId,
cellDataByFieldId: cellDataByFieldId, cellDataByFieldId: cellDataByFieldId,
@ -161,7 +161,7 @@ class DatabaseController {
Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow, Future<Either<Unit, FlowyError>> moveRow(RowPB fromRow,
{RowPB? toRow, String? groupId}) { {RowPB? toRow, String? groupId}) {
return _databaseBackendSvc.moveRow( return _databaseViewBackendSvc.moveRow(
fromRowId: fromRow.id, fromRowId: fromRow.id,
toGroupId: groupId, toGroupId: groupId,
toRowId: toRow?.id, toRowId: toRow?.id,
@ -170,7 +170,7 @@ class DatabaseController {
Future<Either<Unit, FlowyError>> moveGroup( Future<Either<Unit, FlowyError>> moveGroup(
{required String fromGroupId, required String toGroupId}) { {required String fromGroupId, required String toGroupId}) {
return _databaseBackendSvc.moveGroup( return _databaseViewBackendSvc.moveGroup(
fromGroupId: fromGroupId, fromGroupId: fromGroupId,
toGroupId: toGroupId, toGroupId: toGroupId,
); );
@ -178,7 +178,7 @@ class DatabaseController {
Future<void> updateCalenderLayoutSetting( Future<void> updateCalenderLayoutSetting(
CalendarLayoutSettingsPB layoutSetting) async { CalendarLayoutSettingsPB layoutSetting) async {
await _databaseBackendSvc await _databaseViewBackendSvc
.updateLayoutSetting(calendarLayoutSetting: layoutSetting) .updateLayoutSetting(calendarLayoutSetting: layoutSetting)
.then((result) { .then((result) {
result.fold((l) => null, (r) => Log.error(r)); result.fold((l) => null, (r) => Log.error(r));
@ -186,13 +186,13 @@ class DatabaseController {
} }
Future<void> dispose() async { Future<void> dispose() async {
await _databaseBackendSvc.closeView(); await _databaseViewBackendSvc.closeView();
await fieldController.dispose(); await fieldController.dispose();
await groupListener.stop(); await groupListener.stop();
} }
Future<void> _loadGroups() async { Future<void> _loadGroups() async {
final result = await _databaseBackendSvc.loadGroups(); final result = await _databaseViewBackendSvc.loadGroups();
return Future( return Future(
() => result.fold( () => result.fold(
(groups) { (groups) {
@ -204,7 +204,7 @@ class DatabaseController {
} }
Future<void> _loadLayoutSetting() async { Future<void> _loadLayoutSetting() async {
_databaseBackendSvc.getLayoutSetting(layoutType).then((result) { _databaseViewBackendSvc.getLayoutSetting(layoutType).then((result) {
result.fold( result.fold(
(l) { (l) {
_layoutCallbacks?.onLoadLayout(l); _layoutCallbacks?.onLoadLayout(l);

View File

@ -1,121 +1,13 @@
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
class DatabaseBackendService { class DatabaseBackendService {
final String viewId; static Future<Either<List<DatabaseDescriptionPB>, FlowyError>>
DatabaseBackendService({ getAllDatabase() {
required this.viewId, return DatabaseEventGetDatabases().send().then((result) {
});
Future<Either<DatabasePB, FlowyError>> openGrid() async {
await FolderEventSetLatestView(ViewIdPB(value: viewId)).send();
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
Future<Either<RowPB, FlowyError>> createRow({
String? startRowId,
String? groupId,
Map<String, String>? cellDataByFieldId,
}) {
var payload = CreateRowPayloadPB.create()..viewId = viewId;
if (startRowId != null) {
payload.startRowId = startRowId;
}
if (groupId != null) {
payload.groupId = groupId;
}
if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) {
payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
}
return DatabaseEventCreateRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId,
required String? toGroupId,
required String? toRowId,
}) {
var payload = MoveGroupRowPayloadPB.create()
..viewId = viewId
..fromRowId = fromRowId;
if (toGroupId != null) {
payload.toGroupId = toGroupId;
}
if (toRowId != null) {
payload.toRowId = toRowId;
}
return DatabaseEventMoveGroupRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveGroup({
required String fromGroupId,
required String toGroupId,
}) {
final payload = MoveGroupPayloadPB.create()
..viewId = viewId
..fromGroupId = fromGroupId
..toGroupId = toGroupId;
return DatabaseEventMoveGroup(payload).send();
}
Future<Either<List<FieldPB>, FlowyError>> getFields(
{List<FieldIdPB>? fieldIds}) {
var payload = GetFieldPayloadPB.create()..viewId = viewId;
if (fieldIds != null) {
payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);
}
return DatabaseEventGetFields(payload).send().then((result) {
return result.fold((l) => left(l.items), (r) => right(r)); return result.fold((l) => left(l.items), (r) => right(r));
}); });
} }
Future<Either<LayoutSettingPB, FlowyError>> getLayoutSetting(
LayoutTypePB layoutType) {
final payload = DatabaseLayoutIdPB.create()
..viewId = viewId
..layout = layoutType;
return DatabaseEventGetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> updateLayoutSetting(
{CalendarLayoutSettingsPB? calendarLayoutSetting}) {
final layoutSetting = LayoutSettingPB.create();
if (calendarLayoutSetting != null) {
layoutSetting.calendar = calendarLayoutSetting;
}
final payload = UpdateLayoutSettingPB.create()
..viewId = viewId
..layoutSetting = layoutSetting;
return DatabaseEventSetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> closeView() {
final request = ViewIdPB(value: viewId);
return FolderEventCloseView(request).send();
}
Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetGroups(payload).send();
}
} }

View File

@ -0,0 +1,121 @@
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
class DatabaseViewBackendService {
final String viewId;
DatabaseViewBackendService({
required this.viewId,
});
Future<Either<DatabasePB, FlowyError>> openGrid() async {
await FolderEventSetLatestView(ViewIdPB(value: viewId)).send();
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
Future<Either<RowPB, FlowyError>> createRow({
String? startRowId,
String? groupId,
Map<String, String>? cellDataByFieldId,
}) {
var payload = CreateRowPayloadPB.create()..viewId = viewId;
if (startRowId != null) {
payload.startRowId = startRowId;
}
if (groupId != null) {
payload.groupId = groupId;
}
if (cellDataByFieldId != null && cellDataByFieldId.isNotEmpty) {
payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
}
return DatabaseEventCreateRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveRow({
required String fromRowId,
required String? toGroupId,
required String? toRowId,
}) {
var payload = MoveGroupRowPayloadPB.create()
..viewId = viewId
..fromRowId = fromRowId;
if (toGroupId != null) {
payload.toGroupId = toGroupId;
}
if (toRowId != null) {
payload.toRowId = toRowId;
}
return DatabaseEventMoveGroupRow(payload).send();
}
Future<Either<Unit, FlowyError>> moveGroup({
required String fromGroupId,
required String toGroupId,
}) {
final payload = MoveGroupPayloadPB.create()
..viewId = viewId
..fromGroupId = fromGroupId
..toGroupId = toGroupId;
return DatabaseEventMoveGroup(payload).send();
}
Future<Either<List<FieldPB>, FlowyError>> getFields(
{List<FieldIdPB>? fieldIds}) {
var payload = GetFieldPayloadPB.create()..viewId = viewId;
if (fieldIds != null) {
payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);
}
return DatabaseEventGetFields(payload).send().then((result) {
return result.fold((l) => left(l.items), (r) => right(r));
});
}
Future<Either<LayoutSettingPB, FlowyError>> getLayoutSetting(
LayoutTypePB layoutType) {
final payload = DatabaseLayoutIdPB.create()
..viewId = viewId
..layout = layoutType;
return DatabaseEventGetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> updateLayoutSetting(
{CalendarLayoutSettingsPB? calendarLayoutSetting}) {
final layoutSetting = LayoutSettingPB.create();
if (calendarLayoutSetting != null) {
layoutSetting.calendar = calendarLayoutSetting;
}
final payload = UpdateLayoutSettingPB.create()
..viewId = viewId
..layoutSetting = layoutSetting;
return DatabaseEventSetLayoutSetting(payload).send();
}
Future<Either<Unit, FlowyError>> closeView() {
final request = ViewIdPB(value: viewId);
return FolderEventCloseView(request).send();
}
Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetGroups(payload).send();
}
}

View File

@ -11,7 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../grid/presentation/widgets/filter/filter_info.dart'; import '../../grid/presentation/widgets/filter/filter_info.dart';
import '../../grid/presentation/widgets/sort/sort_info.dart'; import '../../grid/presentation/widgets/sort/sort_info.dart';
import '../database_service.dart'; import '../database_view_service.dart';
import '../filter/filter_listener.dart'; import '../filter/filter_listener.dart';
import '../filter/filter_service.dart'; import '../filter/filter_service.dart';
import '../row/row_cache.dart'; import '../row/row_cache.dart';
@ -80,7 +80,7 @@ class FieldController {
final SortsListener _sortsListener; final SortsListener _sortsListener;
// FFI services // FFI services
final DatabaseBackendService _databaseBackendSvc; final DatabaseViewBackendService _databaseViewBackendSvc;
final SettingBackendService _settingBackendSvc; final SettingBackendService _settingBackendSvc;
final FilterBackendService _filterBackendSvc; final FilterBackendService _filterBackendSvc;
final SortBackendService _sortBackendSvc; final SortBackendService _sortBackendSvc;
@ -152,7 +152,7 @@ class FieldController {
_settingListener = DatabaseSettingListener(viewId: viewId), _settingListener = DatabaseSettingListener(viewId: viewId),
_filterBackendSvc = FilterBackendService(viewId: viewId), _filterBackendSvc = FilterBackendService(viewId: viewId),
_filtersListener = FiltersListener(viewId: viewId), _filtersListener = FiltersListener(viewId: viewId),
_databaseBackendSvc = DatabaseBackendService(viewId: viewId), _databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId),
_sortBackendSvc = SortBackendService(viewId: viewId), _sortBackendSvc = SortBackendService(viewId: viewId),
_sortsListener = SortsListener(viewId: viewId), _sortsListener = SortsListener(viewId: viewId),
_settingBackendSvc = SettingBackendService(viewId: viewId) { _settingBackendSvc = SettingBackendService(viewId: viewId) {
@ -448,7 +448,7 @@ class FieldController {
Future<Either<Unit, FlowyError>> loadFields({ Future<Either<Unit, FlowyError>> loadFields({
required List<FieldIdPB> fieldIds, required List<FieldIdPB> fieldIds,
}) async { }) async {
final result = await _databaseBackendSvc.getFields(fieldIds: fieldIds); final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds);
return Future( return Future(
() => result.fold( () => result.fold(
(newFields) { (newFields) {

View File

@ -183,9 +183,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
], ],
], ],
toolbarItems: [ toolbarItems: [
if (openAIKey != null && openAIKey!.isNotEmpty) ...[ smartEditItem,
smartEditItem,
]
], ],
themeData: theme.copyWith(extensions: [ themeData: theme.copyWith(extensions: [
...theme.extensions.values, ...theme.extensions.values,

View File

@ -59,7 +59,7 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
}, },
future: AppService().getView(appID, gridID), future: AppBackendService().getView(appID, gridID),
); );
} }

View File

@ -169,7 +169,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
); );
} }
}, },
future: AppService().fetchViews(widget.layoutType), future: AppBackendService().fetchViews(widget.layoutType),
); );
} }

View File

@ -2,14 +2,11 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_widget.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' show FileType;
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
@ -75,6 +72,7 @@ class CoverColorPicker extends StatefulWidget {
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> { class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
late Future<List<String>>? fileImages; late Future<List<String>>? fileImages;
bool isAddingImage = false;
@override @override
void initState() { void initState() {
@ -87,26 +85,40 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
return Padding( return Padding(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: isAddingImage
crossAxisAlignment: CrossAxisAlignment.start, ? CoverImagePicker(
children: [ onBackPressed: () => setState(() {
FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()), isAddingImage = false;
const SizedBox(height: 10), }),
_buildColorPickerList(), onFileSubmit: (List<String> path) {
const SizedBox(height: 10), setState(() {
FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()), isAddingImage = false;
const SizedBox(height: 10), });
_buildFileImagePicker(), })
const SizedBox(height: 10), : _buildCoverSelection(),
FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
const SizedBox(height: 10),
_buildAbstractImagePicker(),
],
),
), ),
); );
} }
Widget _buildCoverSelection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(LocaleKeys.document_plugins_cover_colors.tr()),
const SizedBox(height: 10),
_buildColorPickerList(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_images.tr()),
const SizedBox(height: 10),
_buildFileImagePicker(),
const SizedBox(height: 10),
FlowyText.semibold(LocaleKeys.document_plugins_cover_abstract.tr()),
const SizedBox(height: 10),
_buildAbstractImagePicker(),
],
);
}
Widget _buildAbstractImagePicker() { Widget _buildAbstractImagePicker() {
return GridView.builder( return GridView.builder(
shrinkWrap: true, shrinkWrap: true,
@ -196,7 +208,9 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
), ),
width: 20, width: 20,
onPressed: () { onPressed: () {
_pickImages(); setState(() {
isAddingImage = true;
});
}, },
), ),
); );
@ -248,36 +262,6 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
prefs.setStringList(kLocalImagesKey, imageNames); prefs.setStringList(kLocalImagesKey, imageNames);
return imageNames; return imageNames;
} }
Future<void> _pickImages() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> imageNames = prefs.getStringList(kLocalImagesKey) ?? [];
FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'],
);
if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path;
if (path != null) {
final directory = await _coverPath();
final newPath = await File(path).copy(
'$directory/${path.split('/').last}',
);
imageNames.add(newPath.path);
}
}
await prefs.setStringList(kLocalImagesKey, imageNames);
setState(() {});
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory('$directory/covers')
.create(recursive: true)
.then((value) => value.path);
}
} }
class _CoverColorPickerState extends State<CoverColorPicker> { class _CoverColorPickerState extends State<CoverColorPicker> {

View File

@ -0,0 +1,254 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_image_picker_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class CoverImagePicker extends StatefulWidget {
final VoidCallback onBackPressed;
final Function(List<String> paths) onFileSubmit;
const CoverImagePicker(
{super.key, required this.onBackPressed, required this.onFileSubmit});
@override
State<CoverImagePicker> createState() => _CoverImagePickerState();
}
class _CoverImagePickerState extends State<CoverImagePicker> {
TextEditingController urlController = TextEditingController();
bool get buttonDisabled => urlController.text.isEmpty;
@override
void initState() {
super.initState();
urlController.addListener(() {
setState(() {});
});
}
_buildFilePickerWidget(BuildContext ctx) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
svgWidget(
"editor/add",
size: const Size(20, 20),
),
const SizedBox(
width: 3,
),
FlowyText(
LocaleKeys.document_plugins_cover_pasteImageUrl.tr(),
),
],
),
const SizedBox(
height: 10,
),
FlowyText(
LocaleKeys.document_plugins_cover_or.tr(),
color: Colors.grey,
),
const SizedBox(
height: 10,
),
FlowyButton(
onTap: () {
ctx.read<CoverImagePickerBloc>().add(const PickFileImage());
},
useIntrinsicWidth: true,
leftIcon: svgWidget(
"file_icon",
size: const Size(25, 25),
),
text: FlowyText(
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
),
),
],
);
}
_buildImageDeleteButton(BuildContext ctx) {
return Positioned(
right: 10,
top: 10,
child: InkWell(
onTap: () {
ctx.read<CoverImagePickerBloc>().add(const DeleteImage());
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.onPrimary),
child: svgWidget(
"editor/close",
size: const Size(20, 20),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CoverImagePickerBloc()
..add(const CoverImagePickerEvent.initialEvent()),
child: BlocListener<CoverImagePickerBloc, CoverImagePickerState>(
listener: (context, state) {
if (state is NetworkImagePicked) {
state.successOrFail.isRight()
? showSnapBar(context,
LocaleKeys.document_plugins_cover_invalidImageUrl.tr())
: null;
}
if (state is Done) {
state.successOrFail.fold(
(l) => widget.onFileSubmit(l),
(r) => showSnapBar(
context,
LocaleKeys.document_plugins_cover_failedToAddImageToGallery
.tr()));
}
},
child: BlocBuilder<CoverImagePickerBloc, CoverImagePickerState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
state is Loading
? const SizedBox(
height: 180,
child: Center(
child: CircularProgressIndicator(),
),
)
: Stack(
children: [
Container(
height: 180,
alignment: Alignment.center,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondary,
borderRadius: Corners.s6Border,
image: state is Initial
? null
: state is NetworkImagePicked
? state.successOrFail.fold(
(path) => DecorationImage(
image: NetworkImage(path),
fit: BoxFit.cover),
(r) => null)
: state is FileImagePicked
? DecorationImage(
image: FileImage(
File(state.path)),
fit: BoxFit.cover)
: null),
child: (state is Initial)
? _buildFilePickerWidget(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => null,
(r) => _buildFilePickerWidget(
context,
),
)
: null),
(state is FileImagePicked)
? _buildImageDeleteButton(context)
: (state is NetworkImagePicked)
? state.successOrFail.fold(
(l) => _buildImageDeleteButton(context),
(r) => Container())
: Container()
],
),
const SizedBox(
height: 10,
),
Row(
children: [
Expanded(
flex: 4,
child: FlowyTextField(
controller: urlController,
hintText: LocaleKeys
.document_plugins_cover_enterImageUrl
.tr(),
),
),
const SizedBox(
width: 5,
),
Expanded(
flex: 1,
child: RoundedTextButton(
onPressed: () {
urlController.text.isNotEmpty
? context
.read<CoverImagePickerBloc>()
.add(UrlSubmit(urlController.text))
: null;
},
hoverColor: Colors.transparent,
fillColor: buttonDisabled
? Colors.grey
: Theme.of(context).colorScheme.primary,
height: 36,
title: LocaleKeys.document_plugins_cover_add.tr(),
borderRadius: Corners.s8Border,
),
)
],
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FlowyTextButton(
LocaleKeys.document_plugins_cover_back.tr(),
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
onPressed: () => widget.onBackPressed(),
),
FlowyTextButton(
LocaleKeys.document_plugins_cover_saveToGallery.tr(),
onPressed: () async {
context
.read<CoverImagePickerBloc>()
.add(SaveToGallery(state));
},
hoverColor: Colors.transparent,
fillColor: Colors.transparent,
mainAxisAlignment: MainAxisAlignment.end,
fontColor: Theme.of(context).colorScheme.primary,
),
],
)
],
);
},
),
),
);
}
}

View File

@ -0,0 +1,196 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/file_picker/file_picker_service.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart' as fp;
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as path;
import 'change_cover_popover.dart';
part 'cover_image_picker_bloc.freezed.dart';
class CoverImagePickerBloc
extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {
CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {
on<CoverImagePickerEvent>(
(event, emit) async {
await event.map(
initialEvent: (InitialEvent initialEvent) {
emit(const CoverImagePickerState.initial());
},
urlSubmit: (UrlSubmit urlSubmit) async {
emit(const CoverImagePickerState.loading());
final validateImage = await _validateUrl(urlSubmit.path);
if (validateImage) {
emit(CoverImagePickerState.networkImage(left(urlSubmit.path)));
} else {
emit(
CoverImagePickerState.networkImage(
right(
FlowyError(
msg: LocaleKeys.document_plugins_cover_couldNotFetchImage
.tr(),
),
),
),
);
}
},
pickFileImage: (PickFileImage pickFileImage) async {
final imagePickerResults = await _pickImages();
if (imagePickerResults != null) {
emit(CoverImagePickerState.fileImage(imagePickerResults));
} else {
emit(const CoverImagePickerState.initial());
}
},
deleteImage: (DeleteImage deleteImage) {
emit(const CoverImagePickerState.initial());
},
saveToGallery: (SaveToGallery saveToGallery) async {
emit(const CoverImagePickerState.loading());
final saveImage = await _saveToGallery(saveToGallery.previousState);
if (saveImage != null) {
emit(CoverImagePickerState.done(left(saveImage)));
} else {
emit(
CoverImagePickerState.done(
right(
FlowyError(
msg: LocaleKeys.document_plugins_cover_imageSavingFailed
.tr()),
),
),
);
emit(const CoverImagePickerState.initial());
}
},
);
},
);
}
_saveToGallery(CoverImagePickerState state) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
List<String> imagePaths = prefs.getStringList(kLocalImagesKey) ?? [];
final directory = await _coverPath();
if (state is FileImagePicked) {
try {
final path = state.path;
final newPath = '$directory/${path.split("\\").last}';
final newFile = await File(path).copy(newPath);
imagePaths.add(newFile.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} catch (e) {
return null;
}
} else if (state is NetworkImagePicked) {
try {
String? url = state.successOrFail.fold((path) => path, (r) => null);
if (url != null) {
final response = await http.get(Uri.parse(url));
final newPath =
"$directory/IMG_$_timeStampString.${_getExtention(url)}";
final imageFile = File(newPath);
await imageFile.create();
await imageFile.writeAsBytes(response.bodyBytes);
imagePaths.add(imageFile.absolute.path);
await prefs.setStringList(kLocalImagesKey, imagePaths);
return imagePaths;
} else {
return null;
}
} catch (e) {
return null;
}
}
}
_pickImages() async {
FilePickerResult? result = await getIt<FilePickerService>().pickFiles(
dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),
allowMultiple: false,
type: fp.FileType.image,
allowedExtensions: ['jpg', 'png', 'jpeg'],
);
if (result != null && result.files.isNotEmpty) {
final path = result.files.first.path;
if (path != null) {
return path;
} else {
return null;
}
}
return null;
}
Future<String> _coverPath() async {
final directory = await getIt<SettingsLocationCubit>().fetchLocation();
return Directory(path.join(directory, 'covers'))
.create(recursive: true)
.then((value) => value.path);
}
String get _timeStampString =>
DateTime.now().millisecondsSinceEpoch.toString();
String? _getExtention(String path) => path.contains(".jpg")
? "jpg"
: path.contains(".png")
? "png"
: path.contains(".jpeg")
? "jpeg"
: (path.contains("auto=format") && path.contains("unsplash"))
? "jpeg"
: null;
_validateUrl(String path) async {
if (_getExtention(path) != null) {
try {
final response = await http.get(Uri.parse(path));
if (response.statusCode == 200) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
} else {
return false;
}
}
}
@freezed
class CoverImagePickerEvent with _$CoverImagePickerEvent {
const factory CoverImagePickerEvent.urlSubmit(String path) = UrlSubmit;
const factory CoverImagePickerEvent.pickFileImage() = PickFileImage;
const factory CoverImagePickerEvent.deleteImage() = DeleteImage;
const factory CoverImagePickerEvent.saveToGallery(
CoverImagePickerState previousState) = SaveToGallery;
const factory CoverImagePickerEvent.initialEvent() = InitialEvent;
}
@freezed
class CoverImagePickerState with _$CoverImagePickerState {
const factory CoverImagePickerState.initial() = Initial;
const factory CoverImagePickerState.loading() = Loading;
const factory CoverImagePickerState.networkImage(
Either<String, FlowyError> successOrFail) = NetworkImagePicked;
const factory CoverImagePickerState.fileImage(String path) = FileImagePicked;
const factory CoverImagePickerState.done(
Either<List<String>, FlowyError> successOrFail) = Done;
}

View File

@ -2,6 +2,9 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart'; import 'package:appflowy/plugins/document/presentation/plugins/cover/change_cover_popover.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/emoji_popover.dart';
import 'package:appflowy/plugins/document/presentation/plugins/cover/icon_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -14,10 +17,10 @@ import 'package:flutter/material.dart';
const String kCoverType = 'cover'; const String kCoverType = 'cover';
const String kCoverSelectionTypeAttribute = 'cover_selection_type'; const String kCoverSelectionTypeAttribute = 'cover_selection_type';
const String kCoverSelectionAttribute = 'cover_selection'; const String kCoverSelectionAttribute = 'cover_selection';
const String kIconSelectionAttribute = 'selected_icon';
enum CoverSelectionType { enum CoverSelectionType {
initial, initial,
color, color,
file, file,
asset; asset;
@ -68,23 +71,16 @@ class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
widget.node.attributes[kCoverSelectionTypeAttribute], widget.node.attributes[kCoverSelectionTypeAttribute],
); );
PopoverController iconPopoverController = PopoverController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (selectionType == CoverSelectionType.initial) { return _CoverImage(
return _AddCoverButton( editorState: widget.editorState,
onTap: () { node: widget.node,
_insertCover(CoverSelectionType.asset, builtInAssetImages.first); onCoverChanged: (type, value) {
}, _insertCover(type, value);
); },
} else { );
return _CoverImage(
editorState: widget.editorState,
node: widget.node,
onCoverChanged: (type, value) {
_insertCover(type, value);
},
);
}
} }
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async { Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
@ -92,14 +88,26 @@ class _CoverImageNodeWidgetState extends State<_CoverImageNodeWidget> {
transaction.updateNode(widget.node, { transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute: type.toString(), kCoverSelectionTypeAttribute: type.toString(),
kCoverSelectionAttribute: cover, kCoverSelectionAttribute: cover,
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
}); });
return widget.editorState.apply(transaction); return widget.editorState.apply(transaction);
} }
} }
class _AddCoverButton extends StatefulWidget { class _AddCoverButton extends StatefulWidget {
final Node node;
final EditorState editorState;
final bool hasIcon;
final CoverSelectionType selectionType;
final PopoverController iconPopoverController;
const _AddCoverButton({ const _AddCoverButton({
required this.onTap, required this.onTap,
required this.node,
required this.editorState,
required this.hasIcon,
required this.selectionType,
required this.iconPopoverController,
}); });
final VoidCallback onTap; final VoidCallback onTap;
@ -108,8 +116,16 @@ class _AddCoverButton extends StatefulWidget {
State<_AddCoverButton> createState() => _AddCoverButtonState(); State<_AddCoverButton> createState() => _AddCoverButtonState();
} }
bool isPopoverOpen = false;
class _AddCoverButtonState extends State<_AddCoverButton> { class _AddCoverButtonState extends State<_AddCoverButton> {
bool isHidden = true; bool isHidden = true;
PopoverMutex mutex = PopoverMutex();
bool isPopoverOpen = false;
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -118,40 +134,118 @@ class _AddCoverButtonState extends State<_AddCoverButton> {
setHidden(false); setHidden(false);
}, },
onExit: (event) { onExit: (event) {
setHidden(true); setHidden(isPopoverOpen ? false : true);
}, },
opaque: false,
child: Container( child: Container(
height: 50.0, height: widget.hasIcon ? 180 : 50.0,
alignment: Alignment.bottomLeft,
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.only(top: 20, bottom: 5), padding: const EdgeInsets.only(top: 20, bottom: 5),
// color: Colors.red,
child: isHidden child: isHidden
? const SizedBox() ? Container()
: Row( : Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// Add Cover Button. // Add Cover Button.
FlowyButton( widget.selectionType != CoverSelectionType.initial
leftIconSize: const Size.square(18), ? Container()
onTap: widget.onTap, : FlowyButton(
useIntrinsicWidth: true, key: UniqueKey(),
leftIcon: svgWidget( leftIconSize: const Size.square(18),
'editor/image', onTap: widget.onTap,
color: Theme.of(context).colorScheme.onSurface, useIntrinsicWidth: true,
), leftIcon: svgWidget(
text: FlowyText.regular( 'editor/image',
LocaleKeys.document_plugins_cover_addCover.tr(), color: Theme.of(context).colorScheme.onSurface,
), ),
) text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addCover.tr(),
),
),
// Add Icon Button. // Add Icon Button.
// ... widget.hasIcon
? FlowyButton(
leftIconSize: const Size.square(18),
onTap: () {
_removeIcon();
},
useIntrinsicWidth: true,
leftIcon: Icon(
Icons.emoji_emotions_outlined,
color: Theme.of(context).colorScheme.onSurface,
size: 18,
),
text: FlowyText.regular(LocaleKeys
.document_plugins_cover_removeIcon
.tr()),
)
: AppFlowyPopover(
mutex: mutex,
asBarrier: true,
onClose: () {
isPopoverOpen = false;
setHidden(true);
},
offset: const Offset(120, 10),
controller: widget.iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints:
BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: FlowyButton(
leftIconSize: const Size.square(18),
useIntrinsicWidth: true,
leftIcon: Icon(Icons.emoji_emotions_outlined,
color: Theme.of(context).colorScheme.onSurface,
size: 18),
text: FlowyText.regular(
LocaleKeys.document_plugins_cover_addIcon.tr()),
),
popupBuilder: (BuildContext popoverContext) {
isPopoverOpen = true;
return EmojiPopover(
showRemoveButton: widget.hasIcon,
removeIcon: _removeIcon,
node: widget.node,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
widget.iconPopoverController.close();
});
},
)
], ],
), ),
), ),
); );
} }
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
kIconSelectionAttribute: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kIconSelectionAttribute: "",
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
});
return widget.editorState.apply(transaction);
}
void setHidden(bool value) { void setHidden(bool value) {
if (isHidden == value) return; if (isHidden == value) return;
setState(() { setState(() {
@ -173,7 +267,6 @@ class _CoverImage extends StatefulWidget {
CoverSelectionType selectionType, CoverSelectionType selectionType,
dynamic selection, dynamic selection,
) onCoverChanged; ) onCoverChanged;
@override @override
State<_CoverImage> createState() => _CoverImageState(); State<_CoverImage> createState() => _CoverImageState();
} }
@ -187,23 +280,112 @@ class _CoverImageState extends State<_CoverImage> {
Color get color => Color get color =>
Color(int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ?? Color(int.tryParse(widget.node.attributes[kCoverSelectionAttribute]) ??
0xFFFFFFFF); 0xFFFFFFFF);
bool get hasIcon => widget.node.attributes[kIconSelectionAttribute] == null
? false
: widget.node.attributes[kIconSelectionAttribute].isNotEmpty;
bool isOverlayButtonsHidden = true; bool isOverlayButtonsHidden = true;
PopoverController iconPopoverController = PopoverController();
bool get hasCover =>
selectionType == CoverSelectionType.initial ? false : true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
alignment: Alignment.bottomLeft,
children: [ children: [
_buildCoverImage(context, widget.editorState), Container(
_buildCoverOverlayButtons(context), alignment: Alignment.topCenter,
height: !hasCover
? 0
: hasIcon
? 320
: 280,
child: _buildCoverImage(context, widget.editorState),
),
hasIcon
? Positioned(
bottom: !hasCover ? 30 : 10,
child: AppFlowyPopover(
offset: const Offset(100, 0),
controller: iconPopoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: BoxConstraints.loose(const Size(320, 380)),
margin: EdgeInsets.zero,
child: EmojiIconWidget(
emoji: widget.node.attributes[kIconSelectionAttribute],
onEmojiTapped: () {
iconPopoverController.show();
},
),
popupBuilder: (BuildContext popoverContext) {
return EmojiPopover(
node: widget.node,
showRemoveButton: hasIcon,
removeIcon: _removeIcon,
editorState: widget.editorState,
onEmojiChanged: (Emoji emoji) {
_insertIcon(emoji);
iconPopoverController.close();
});
},
),
)
: Container(),
hasIcon && selectionType != CoverSelectionType.initial
? Container()
: _AddCoverButton(
onTap: () {
_insertCover(
CoverSelectionType.asset, builtInAssetImages.first);
},
node: widget.node,
editorState: widget.editorState,
hasIcon: hasIcon,
selectionType: selectionType,
iconPopoverController: iconPopoverController,
),
], ],
); );
} }
Future<void> _insertCover(CoverSelectionType type, dynamic cover) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute: type.toString(),
kCoverSelectionAttribute: cover,
kIconSelectionAttribute: widget.node.attributes[kIconSelectionAttribute]
});
return widget.editorState.apply(transaction);
}
Future<void> _insertIcon(Emoji emoji) async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
kIconSelectionAttribute: emoji.emoji,
});
return widget.editorState.apply(transaction);
}
Future<void> _removeIcon() async {
final transaction = widget.editorState.transaction;
transaction.updateNode(widget.node, {
kIconSelectionAttribute: "",
kCoverSelectionTypeAttribute:
widget.node.attributes[kCoverSelectionTypeAttribute],
kCoverSelectionAttribute:
widget.node.attributes[kCoverSelectionAttribute],
});
return widget.editorState.apply(transaction);
}
Widget _buildCoverOverlayButtons(BuildContext context) { Widget _buildCoverOverlayButtons(BuildContext context) {
return Positioned( return Positioned(
bottom: 22, bottom: 20,
right: 12, right: 260,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -253,7 +435,7 @@ class _CoverImageState extends State<_CoverImage> {
Widget _buildCoverImage(BuildContext context, EditorState editorState) { Widget _buildCoverImage(BuildContext context, EditorState editorState) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
const height = 200.0; const height = 250.0;
final Widget coverImage; final Widget coverImage;
switch (selectionType) { switch (selectionType) {
case CoverSelectionType.file: case CoverSelectionType.file:
@ -278,7 +460,7 @@ class _CoverImageState extends State<_CoverImage> {
); );
break; break;
case CoverSelectionType.initial: case CoverSelectionType.initial:
coverImage = const SizedBox(); // just an empty sizebox coverImage = const SizedBox();
break; break;
} }
//OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr //OverflowBox needs to be wraped by a widget with constraints(or from its parent) first,otherwise it will occur an erorr
@ -286,11 +468,16 @@ class _CoverImageState extends State<_CoverImage> {
height: height, height: height,
child: OverflowBox( child: OverflowBox(
maxWidth: screenSize.width, maxWidth: screenSize.width,
child: Container( child: Stack(
padding: const EdgeInsets.only(bottom: 10), children: [
height: double.infinity, Container(
width: double.infinity, padding: const EdgeInsets.only(bottom: 10),
child: coverImage, height: double.infinity,
width: double.infinity,
child: coverImage,
),
hasCover ? _buildCoverOverlayButtons(context) : const SizedBox()
],
), ),
), ),
); );

View File

@ -0,0 +1,92 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart';
import 'package:appflowy/workspace/presentation/widgets/emoji_picker/src/emoji_view_state.dart';
import 'package:appflowy_editor/appflowy_editor.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:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class EmojiPopover extends StatefulWidget {
final EditorState editorState;
final Node node;
final void Function(Emoji emoji) onEmojiChanged;
final VoidCallback removeIcon;
final bool showRemoveButton;
const EmojiPopover({
super.key,
required this.editorState,
required this.node,
required this.onEmojiChanged,
required this.removeIcon,
required this.showRemoveButton,
});
@override
State<EmojiPopover> createState() => _EmojiPopoverState();
}
class _EmojiPopoverState extends State<EmojiPopover> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(15),
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
widget.onEmojiChanged(emoji);
},
customWidget: (Config config, EmojiViewState state) {
return Stack(
alignment: Alignment.topRight,
children: [
Container(
padding: EdgeInsets.only(top: widget.showRemoveButton ? 25 : 0),
child: DefaultEmojiPickerView(config, state),
),
_buildDeleteButtonIfNeed(),
],
);
},
config: const Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Colors.transparent,
iconColor: Colors.grey,
iconColorSelected: Color(0xff333333),
indicatorColor: Color(0xff333333),
progressIndicatorColor: Color(0xff333333),
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
);
}
Widget _buildDeleteButtonIfNeed() {
if (!widget.showRemoveButton) {
return const SizedBox();
}
return FlowyButton(
onTap: () => widget.removeIcon(),
useIntrinsicWidth: true,
hoverColor: Theme.of(context).colorScheme.onPrimary,
text: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
svgWidget("editor/delete"),
const SizedBox(
width: 5,
),
FlowyText(
LocaleKeys.document_plugins_cover_removeIcon.tr(),
color: Colors.grey,
),
],
),
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmojiIconWidget extends StatefulWidget {
final String? emoji;
final void Function() onEmojiTapped;
const EmojiIconWidget({
super.key,
required this.emoji,
required this.onEmojiTapped,
});
@override
State<EmojiIconWidget> createState() => _EmojiIconWidgetState();
}
class _EmojiIconWidgetState extends State<EmojiIconWidget> {
bool hover = true;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setHidden(false);
},
onExit: (event) {
setHidden(true);
},
child: Container(
height: 130,
width: 130,
margin: const EdgeInsets.only(top: 18),
decoration: BoxDecoration(
color: !hover
? Theme.of(context).colorScheme.secondary
: Colors.transparent,
borderRadius: Corners.s8Border,
),
alignment: Alignment.center,
child: Stack(
clipBehavior: Clip.none,
children: [
FlowyText(
widget.emoji.toString(),
fontSize: 80,
),
],
),
),
);
}
void setHidden(bool value) {
if (hover == value) return;
setState(() {
hover = value;
});
}
}

View File

@ -10,9 +10,9 @@ enum SmartEditAction {
String get toInstruction { String get toInstruction {
switch (this) { switch (this) {
case SmartEditAction.summarize: case SmartEditAction.summarize:
return 'Make it shorter'; return 'Make this shorter and more concise:';
case SmartEditAction.fixSpelling: case SmartEditAction.fixSpelling:
return 'Fix all the spelling mistakes'; return 'Correct this to standard English:';
} }
} }
} }

View File

@ -140,9 +140,12 @@ class _SmartEditInputState extends State<_SmartEditInput> {
} }
Widget _buildResultWidget(BuildContext context) { Widget _buildResultWidget(BuildContext context) {
final loading = SizedBox.fromSize( final loading = Padding(
size: const Size.square(14), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: const CircularProgressIndicator(), child: SizedBox.fromSize(
size: const Size.square(14),
child: const CircularProgressIndicator(),
),
); );
if (result == null) { if (result == null) {
return loading; return loading;
@ -222,7 +225,6 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final texts = result!.asRight().choices.first.text.split('\n') final texts = result!.asRight().choices.first.text.split('\n')
..removeWhere((element) => element.isEmpty); ..removeWhere((element) => element.isEmpty);
assert(texts.length == selectedNodes.length);
final transaction = widget.editorState.transaction; final transaction = widget.editorState.transaction;
transaction.replaceTexts( transaction.replaceTexts(
selectedNodes.toList(growable: false), selectedNodes.toList(growable: false),
@ -254,7 +256,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
final edits = await openAIRepository.getEdits( final edits = await openAIRepository.getEdits(
input: input, input: input,
instruction: instruction, instruction: instruction,
n: input.split('\n').length, n: 1,
); );
return edits.fold((error) async { return edits.fold((error) async {
return dartz.Left( return dartz.Left(

View File

@ -1,10 +1,14 @@
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
ToolbarItem smartEditItem = ToolbarItem( ToolbarItem smartEditItem = ToolbarItem(
id: 'appflowy.toolbar.smart_edit', id: 'appflowy.toolbar.smart_edit',
@ -33,6 +37,20 @@ class _SmartEditWidget extends StatefulWidget {
} }
class _SmartEditWidgetState extends State<_SmartEditWidget> { class _SmartEditWidgetState extends State<_SmartEditWidget> {
bool isOpenAIEnabled = false;
@override
void initState() {
super.initState();
UserBackendService.getCurrentUserProfile().then((value) {
setState(() {
isOpenAIEnabled =
value.fold((l) => l.openaiKey.isNotEmpty, (r) => false);
});
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopoverActionList<SmartEditActionWrapper>( return PopoverActionList<SmartEditActionWrapper>(
@ -43,7 +61,9 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
buildChild: (controller) { buildChild: (controller) {
return FlowyIconButton( return FlowyIconButton(
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
tooltipText: 'Smart Edit', tooltipText: isOpenAIEnabled
? LocaleKeys.document_plugins_smartEdit.tr()
: LocaleKeys.document_plugins_smartEditDisabled.tr(),
preferBelow: false, preferBelow: false,
icon: const Icon( icon: const Icon(
Icons.lightbulb_outline, Icons.lightbulb_outline,
@ -51,7 +71,11 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
color: Colors.white, color: Colors.white,
), ),
onPressed: () { onPressed: () {
controller.show(); if (isOpenAIEnabled) {
controller.show();
} else {
_showError(LocaleKeys.document_plugins_smartEditDisabled.tr());
}
}, },
); );
}, },
@ -97,4 +121,18 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
withUpdateCursor: false, withUpdateCursor: false,
); );
} }
Future<void> _showError(String message) async {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: LocaleKeys.button_Cancel.tr(),
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
content: FlowyText(message),
),
);
}
} }

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import '../startup.dart'; import '../startup.dart';
@ -35,11 +36,11 @@ Future<Directory> appFlowyDocumentDirectory() async {
switch (integrationEnv()) { switch (integrationEnv()) {
case IntegrationMode.develop: case IntegrationMode.develop:
Directory documentsDir = await getApplicationDocumentsDirectory(); Directory documentsDir = await getApplicationDocumentsDirectory();
return Directory('${documentsDir.path}/flowy_dev').create(); return Directory(path.join(documentsDir.path, 'flowy_dev')).create();
case IntegrationMode.release: case IntegrationMode.release:
Directory documentsDir = await getApplicationDocumentsDirectory(); Directory documentsDir = await getApplicationDocumentsDirectory();
return Directory('${documentsDir.path}/flowy').create(); return Directory(path.join(documentsDir.path, 'flowy')).create();
case IntegrationMode.test: case IntegrationMode.test:
return Directory("${Directory.current.path}/.sandbox"); return Directory(path.join(Directory.current.path, '.sandbox'));
} }
} }

View File

@ -18,11 +18,11 @@ import 'package:dartz/dartz.dart';
part 'app_bloc.freezed.dart'; part 'app_bloc.freezed.dart';
class AppBloc extends Bloc<AppEvent, AppState> { class AppBloc extends Bloc<AppEvent, AppState> {
final AppService appService; final AppBackendService appService;
final AppListener appListener; final AppListener appListener;
AppBloc({required AppPB app}) AppBloc({required AppPB app})
: appService = AppService(), : appService = AppBackendService(),
appListener = AppListener(appId: app.id), appListener = AppListener(appId: app.id),
super(AppState.initial(app)) { super(AppState.initial(app)) {
on<AppEvent>((event, emit) async { on<AppEvent>((event, emit) async {

View File

@ -8,7 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/app.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
class AppService { class AppBackendService {
Future<Either<AppPB, FlowyError>> readApp({required String appId}) { Future<Either<AppPB, FlowyError>> readApp({required String appId}) {
final payload = AppIdPB.create()..value = appId; final payload = AppIdPB.create()..value = appId;
@ -21,9 +21,18 @@ class AppService {
String? desc, String? desc,
required ViewLayoutTypePB layoutType, required ViewLayoutTypePB layoutType,
/// The initial data should be the JSON of the doucment /// The initial data should be the JSON of the document.
/// For example: {"document":{"type":"editor","children":[]}} /// Currently, only support create document with initial data.
///
/// The initial data must be follow this format as shown below.
/// {"document":{"type":"editor","children":[]}}
String? initialData, String? initialData,
/// The [ext] is used to pass through the custom configuration
/// to the backend.
/// Linking the view to the existing database, it needs to pass
/// the database id. For example: "database_id": "xxx"
///
Map<String, String> ext = const {}, Map<String, String> ext = const {},
}) { }) {
final payload = CreateViewPayloadPB.create() final payload = CreateViewPayloadPB.create()

View File

@ -13,11 +13,11 @@ class ViewSectionBloc extends Bloc<ViewSectionEvent, ViewSectionState> {
void Function()? _viewsListener; void Function()? _viewsListener;
void Function()? _selectedViewlistener; void Function()? _selectedViewlistener;
final AppViewDataContext _appViewData; final AppViewDataContext _appViewData;
late final AppService _appService; late final AppBackendService _appService;
ViewSectionBloc({ ViewSectionBloc({
required AppViewDataContext appViewData, required AppViewDataContext appViewData,
}) : _appService = AppService(), }) : _appService = AppBackendService(),
_appViewData = appViewData, _appViewData = appViewData,
super(ViewSectionState.initial(appViewData)) { super(ViewSectionState.initial(appViewData)) {
on<ViewSectionEvent>((event, emit) async { on<ViewSectionEvent>((event, emit) async {

View File

@ -57,7 +57,10 @@ extension CommandExtension on EditorState {
Selection selection, Selection selection,
) { ) {
List<String> res = []; List<String> res = [];
if (!selection.isCollapsed) { if (selection.isSingle) {
final plainText = textNodes.first.toPlainText();
res.add(plainText.substring(selection.startIndex, selection.endIndex));
} else if (!selection.isCollapsed) {
for (var i = 0; i < textNodes.length; i++) { for (var i = 0; i < textNodes.length; i++) {
final plainText = textNodes[i].toPlainText(); final plainText = textNodes[i].toPlainText();
if (i == 0) { if (i == 0) {

View File

@ -27,10 +27,10 @@ class NodeIterator implements Iterator<Node> {
return true; return true;
} }
final node = _currentNode; if (_currentNode == null) {
if (node == null) {
return false; return false;
} }
Node node = _currentNode!;
if (endNode != null && endNode == node) { if (endNode != null && endNode == node) {
_currentNode = null; _currentNode = null;
@ -38,16 +38,19 @@ class NodeIterator implements Iterator<Node> {
} }
if (node.children.isNotEmpty) { if (node.children.isNotEmpty) {
_currentNode = _findLeadingChild(node); _currentNode = node.children.first;
} else if (node.next != null) { } else if (node.next != null) {
_currentNode = node.next!; _currentNode = node.next!;
} else { } else {
final parent = node.parent!; while (node.parent != null) {
final nextOfParent = parent.next; node = node.parent!;
if (nextOfParent == null) { final nextOfParent = node.next;
_currentNode = null; if (nextOfParent == null) {
} else { _currentNode = null;
_currentNode = nextOfParent; } else {
_currentNode = nextOfParent;
break;
}
} }
} }
@ -61,11 +64,4 @@ class NodeIterator implements Iterator<Node> {
} }
return result; return result;
} }
Node _findLeadingChild(Node node) {
while (node.children.isNotEmpty) {
node = node.children.first;
}
return node;
}
} }

View File

@ -291,46 +291,125 @@ extension TextTransaction on Transaction {
Selection selection, Selection selection,
List<String> texts, List<String> texts,
) { ) {
if (textNodes.isEmpty) { if (textNodes.isEmpty || texts.isEmpty) {
return; return;
} }
if (selection.isSingle) { if (textNodes.length == texts.length) {
assert(textNodes.length == 1 && texts.length == 1);
replaceText(
textNodes.first,
selection.startIndex,
selection.length,
texts.first,
);
} else {
final length = textNodes.length; final length = textNodes.length;
for (var i = 0; i < length; i++) {
if (length == 1) {
replaceText(
textNodes.first,
selection.startIndex,
selection.endIndex - selection.startIndex,
texts.first,
);
return;
}
for (var i = 0; i < textNodes.length; i++) {
final textNode = textNodes[i]; final textNode = textNodes[i];
final text = texts[i];
if (i == 0) { if (i == 0) {
replaceText( replaceText(
textNode, textNode,
selection.startIndex, selection.startIndex,
textNode.toPlainText().length, textNode.toPlainText().length,
text, texts.first,
); );
} else if (i == length - 1) { } else if (i == length - 1) {
replaceText( replaceText(
textNode, textNode,
0, 0,
selection.endIndex, selection.endIndex,
text, texts.last,
); );
} else { } else {
replaceText( replaceText(
textNode, textNode,
0, 0,
textNode.toPlainText().length, textNode.toPlainText().length,
text, texts[i],
); );
} }
} }
return;
}
if (textNodes.length > texts.length) {
final length = textNodes.length;
for (var i = 0; i < textNodes.length; i++) {
final textNode = textNodes[i];
if (i == 0) {
replaceText(
textNode,
selection.startIndex,
textNode.toPlainText().length,
texts.first,
);
} else if (i == length - 1) {
replaceText(
textNode,
0,
selection.endIndex,
texts.last,
);
} else {
if (i < texts.length - 1) {
replaceText(
textNode,
0,
textNode.toPlainText().length,
texts[i],
);
} else {
deleteNode(textNode);
}
}
}
afterSelection = null;
return;
}
if (textNodes.length < texts.length) {
final length = texts.length;
for (var i = 0; i < texts.length; i++) {
final text = texts[i];
if (i == 0) {
replaceText(
textNodes.first,
selection.startIndex,
textNodes.first.toPlainText().length,
text,
);
} else if (i == length - 1) {
replaceText(
textNodes.last,
0,
selection.endIndex,
text,
);
} else {
if (i < textNodes.length - 1) {
replaceText(
textNodes[i],
0,
textNodes[i].toPlainText().length,
text,
);
} else {
var path = textNodes.first.path;
var j = i - textNodes.length + length - 1;
while (j > 0) {
path = path.next;
j--;
}
insertNode(path, TextNode(delta: Delta()..insert(text)));
}
}
}
afterSelection = null;
return;
} }
} }
} }

View File

@ -112,6 +112,7 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
isFocus = false; isFocus = false;
this.showCursor = showCursor; this.showCursor = showCursor;
_focusNode.unfocus(disposition: disposition); _focusNode.unfocus(disposition: disposition);
_onFocusChange(false);
} }
@override @override

View File

@ -347,8 +347,9 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
void _onPanStart(DragStartDetails details) { void _onPanStart(DragStartDetails details) {
clearSelection(); clearSelection();
_clearToolbar();
_panStartOffset = details.globalPosition; _panStartOffset = details.globalPosition.translate(-3.0, 0);
_panStartScrollDy = editorState.service.scrollService?.dy; _panStartScrollDy = editorState.service.scrollService?.dy;
_enableInteraction(); _enableInteraction();

View File

@ -28,5 +28,29 @@ void main() async {
} }
expect(nodes.moveNext(), false); expect(nodes.moveNext(), false);
}); });
test('toList - when we have at least three level nested nodes (children)',
() {
final root = Node(type: 'root'),
n1 = Node(type: 'node_1'),
n2 = Node(type: 'node_2');
root.insert(n1);
root.insert(n2);
n1.insert(Node(type: 'node_1_1'));
n1.insert(Node(type: 'node_1_2'));
n1.childAtIndex(0)?.insert(Node(type: 'node_1_1_1'));
n1.childAtIndex(1)?.insert(Node(type: 'node_1_2_1'));
final nodes = NodeIterator(
document: Document(root: root),
startNode: root.childAtPath([0])!,
endNode: root.childAtPath([1]),
).toList();
expect(nodes[0].id, n1.id);
expect(nodes[1].id, n1.childAtIndex(0)!.id);
expect(nodes[nodes.length - 1].id, n2.id);
});
}); });
} }

View File

@ -0,0 +1,132 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../infra/test_editor.dart';
Document createEmptyDocument() {
return Document(
root: Node(
type: 'editor',
),
);
}
void main() async {
group('transaction.dart', () {
testWidgets('test replaceTexts, textNodes.length == texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [3], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC');
expect(textNodes[1].toPlainText(), 'ABC');
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
testWidgets('test replaceTexts, textNodes.length > texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 5);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [4], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2, 3, 4]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC');
expect(textNodes[1].toPlainText(), 'ABC');
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
testWidgets('test replaceTexts, textNodes.length < texts.length',
(tester) async {
TestWidgetsFlutterBinding.ensureInitialized();
final editor = tester.editor
..insertTextNode('0123456789')
..insertTextNode('0123456789')
..insertTextNode('0123456789');
await editor.startTesting();
await tester.pumpAndSettle();
expect(editor.documentLength, 3);
final selection = Selection(
start: Position(path: [0], offset: 4),
end: Position(path: [2], offset: 4),
);
final transaction = editor.editorState.transaction;
var textNodes = [0, 1, 2]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
transaction.replaceTexts(textNodes, selection, texts);
editor.editorState.apply(transaction);
await tester.pumpAndSettle();
expect(editor.documentLength, 4);
textNodes = [0, 1, 2, 3]
.map((e) => editor.nodeAtPath([e])!)
.whereType<TextNode>()
.toList(growable: false);
expect(textNodes[0].toPlainText(), '0123ABC');
expect(textNodes[1].toPlainText(), 'ABC');
expect(textNodes[2].toPlainText(), 'ABC');
expect(textNodes[3].toPlainText(), 'ABC456789');
});
});
}

View File

@ -830,7 +830,7 @@ packages:
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View File

@ -89,12 +89,14 @@ dependencies:
google_fonts: ^3.0.1 google_fonts: ^3.0.1
file_picker: <=5.0.0 file_picker: <=5.0.0
percent_indicator: ^4.0.1 percent_indicator: ^4.0.1
appflowy_editor_plugins: appflowy_editor_plugins:
path: packages/appflowy_editor_plugins path: packages/appflowy_editor_plugins
calendar_view: ^1.0.1 calendar_view: ^1.0.1
window_manager: ^0.3.0 window_manager: ^0.3.0
http: ^0.13.5 http: ^0.13.5
json_annotation: ^4.7.0 json_annotation: ^4.7.0
path: ^1.8.2
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1

View File

@ -30,7 +30,7 @@ class AppFlowyBoardTest {
Future<BoardTestContext> createTestBoard() async { Future<BoardTestContext> createTestBoard() async {
final app = await unitTest.createTestApp(); final app = await unitTest.createTestApp();
final builder = BoardPluginBuilder(); final builder = BoardPluginBuilder();
return AppService() return AppBackendService()
.createView( .createView(
appId: app.id, appId: app.id,
name: "Test Board", name: "Test Board",

View File

@ -8,7 +8,7 @@ import '../util.dart';
Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async { Future<GridTestContext> createTestFilterGrid(AppFlowyGridTest gridTest) async {
final app = await gridTest.unitTest.createTestApp(); final app = await gridTest.unitTest.createTestApp();
final builder = GridPluginBuilder(); final builder = GridPluginBuilder();
final context = await AppService() final context = await AppBackendService()
.createView( .createView(
appId: app.id, appId: app.id,
name: "Filter Grid", name: "Filter Grid",

View File

@ -165,7 +165,7 @@ class AppFlowyGridTest {
Future<GridTestContext> createTestGrid() async { Future<GridTestContext> createTestGrid() async {
final app = await unitTest.createTestApp(); final app = await unitTest.createTestApp();
final builder = GridPluginBuilder(); final builder = GridPluginBuilder();
final context = await AppService() final context = await AppBackendService()
.createView( .createView(
appId: app.id, appId: app.id,
name: "Test Grid", name: "Test Grid",

View File

@ -155,7 +155,7 @@ impl TryInto<MoveGroupRowParams> for MoveGroupRowPayloadPB {
} }
#[derive(Debug, Default, ProtoBuf)] #[derive(Debug, Default, ProtoBuf)]
pub struct DatabaseDescPB { pub struct DatabaseDescriptionPB {
#[pb(index = 1)] #[pb(index = 1)]
pub name: String, pub name: String,
@ -164,9 +164,9 @@ pub struct DatabaseDescPB {
} }
#[derive(Debug, Default, ProtoBuf)] #[derive(Debug, Default, ProtoBuf)]
pub struct RepeatedDatabaseDescPB { pub struct RepeatedDatabaseDescriptionPB {
#[pb(index = 1)] #[pb(index = 1)]
pub items: Vec<DatabaseDescPB>, pub items: Vec<DatabaseDescriptionPB>,
} }
#[derive(Debug, Clone, Default, ProtoBuf)] #[derive(Debug, Clone, Default, ProtoBuf)]

View File

@ -575,17 +575,17 @@ pub(crate) async fn move_group_row_handler(
#[tracing::instrument(level = "debug", skip(manager), err)] #[tracing::instrument(level = "debug", skip(manager), err)]
pub(crate) async fn get_databases_handler( pub(crate) async fn get_databases_handler(
manager: AFPluginState<Arc<DatabaseManager>>, manager: AFPluginState<Arc<DatabaseManager>>,
) -> DataResult<RepeatedDatabaseDescPB, FlowyError> { ) -> DataResult<RepeatedDatabaseDescriptionPB, FlowyError> {
let items = manager let items = manager
.get_databases() .get_databases()
.await? .await?
.into_iter() .into_iter()
.map(|database_info| DatabaseDescPB { .map(|database_info| DatabaseDescriptionPB {
name: database_info.name, name: database_info.name,
database_id: database_info.database_id, database_id: database_info.database_id,
}) })
.collect::<Vec<DatabaseDescPB>>(); .collect::<Vec<DatabaseDescriptionPB>>();
data_result_ok(RepeatedDatabaseDescPB { items }) data_result_ok(RepeatedDatabaseDescriptionPB { items })
} }
#[tracing::instrument(level = "debug", skip(data, manager), err)] #[tracing::instrument(level = "debug", skip(data, manager), err)]

View File

@ -238,7 +238,8 @@ pub enum DatabaseEvent {
#[event(input = "MoveGroupRowPayloadPB")] #[event(input = "MoveGroupRowPayloadPB")]
GroupByField = 113, GroupByField = 113,
#[event(output = "RepeatedDatabaseDescPB")] /// Returns all the databases
#[event(output = "RepeatedDatabaseDescriptionPB")]
GetDatabases = 114, GetDatabases = 114,
#[event(input = "UpdateLayoutSettingPB")] #[event(input = "UpdateLayoutSettingPB")]

View File

@ -7,7 +7,9 @@ use crate::services::database_view::{
make_database_view_rev_manager, make_database_view_revision_pad, DatabaseViewEditor, make_database_view_rev_manager, make_database_view_revision_pad, DatabaseViewEditor,
}; };
use crate::services::persistence::block_index::BlockRowIndexer; use crate::services::persistence::block_index::BlockRowIndexer;
use crate::services::persistence::database_ref::{DatabaseInfo, DatabaseRef, DatabaseRefIndexer}; use crate::services::persistence::database_ref::{
DatabaseInfo, DatabaseRefIndexer, DatabaseViewRef,
};
use crate::services::persistence::kv::DatabaseKVPersistence; use crate::services::persistence::kv::DatabaseKVPersistence;
use crate::services::persistence::migration::DatabaseMigration; use crate::services::persistence::migration::DatabaseMigration;
use crate::services::persistence::rev_sqlite::{ use crate::services::persistence::rev_sqlite::{
@ -192,7 +194,10 @@ impl DatabaseManager {
self.database_ref_indexer.get_all_databases() self.database_ref_indexer.get_all_databases()
} }
pub async fn get_database_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> { pub async fn get_database_ref_views(
&self,
database_id: &str,
) -> FlowyResult<Vec<DatabaseViewRef>> {
self self
.database_ref_indexer .database_ref_indexer
.get_ref_views_with_database(database_id) .get_ref_views_with_database(database_id)
@ -425,13 +430,13 @@ pub async fn create_new_database(
} }
impl DatabaseRefIndexerQuery for DatabaseRefIndexer { impl DatabaseRefIndexerQuery for DatabaseRefIndexer {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> { fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>> {
self.get_ref_views_with_database(database_id) self.get_ref_views_with_database(database_id)
} }
} }
impl DatabaseRefIndexerQuery for Arc<DatabaseRefIndexer> { impl DatabaseRefIndexerQuery for Arc<DatabaseRefIndexer> {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> { fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>> {
(**self).get_ref_views(database_id) (**self).get_ref_views(database_id)
} }
} }

View File

@ -18,7 +18,7 @@ use crate::services::database_view::{
}; };
use crate::services::filter::FilterType; use crate::services::filter::FilterType;
use crate::services::persistence::block_index::BlockRowIndexer; use crate::services::persistence::block_index::BlockRowIndexer;
use crate::services::persistence::database_ref::DatabaseRef; use crate::services::persistence::database_ref::DatabaseViewRef;
use crate::services::row::{DatabaseBlockRow, DatabaseBlockRowRevision, RowRevisionBuilder}; use crate::services::row::{DatabaseBlockRow, DatabaseBlockRowRevision, RowRevisionBuilder};
use bytes::Bytes; use bytes::Bytes;
use database_model::*; use database_model::*;
@ -42,7 +42,7 @@ use std::sync::Arc;
use tokio::sync::{broadcast, RwLock}; use tokio::sync::{broadcast, RwLock};
pub trait DatabaseRefIndexerQuery: Send + Sync + 'static { pub trait DatabaseRefIndexerQuery: Send + Sync + 'static {
fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>>; fn get_ref_views(&self, database_id: &str) -> FlowyResult<Vec<DatabaseViewRef>>;
} }
pub struct DatabaseEditor { pub struct DatabaseEditor {

View File

@ -45,7 +45,10 @@ impl DatabaseRefIndexer {
Ok(()) Ok(())
} }
pub fn get_ref_views_with_database(&self, database_id: &str) -> FlowyResult<Vec<DatabaseRef>> { pub fn get_ref_views_with_database(
&self,
database_id: &str,
) -> FlowyResult<Vec<DatabaseViewRef>> {
let conn = self.database.get_db_connection()?; let conn = self.database.get_db_connection()?;
let views = dsl::database_refs let views = dsl::database_refs
.filter(database_refs::database_id.like(database_id)) .filter(database_refs::database_id.like(database_id))
@ -93,12 +96,12 @@ struct DatabaseRefRecord {
database_id: String, database_id: String,
} }
pub struct DatabaseRef { pub struct DatabaseViewRef {
pub view_id: String, pub view_id: String,
pub name: String, pub name: String,
pub database_id: String, pub database_id: String,
} }
impl std::convert::From<DatabaseRefRecord> for DatabaseRef { impl std::convert::From<DatabaseRefRecord> for DatabaseViewRef {
fn from(record: DatabaseRefRecord) -> Self { fn from(record: DatabaseRefRecord) -> Self {
Self { Self {
view_id: record.view_id, view_id: record.view_id,

View File

@ -2,12 +2,12 @@ use crate::database::block_test::util::DatabaseRowTestBuilder;
use crate::database::database_editor::DatabaseEditorTest; use crate::database::database_editor::DatabaseEditorTest;
use database_model::RowRevision; use database_model::RowRevision;
use flowy_database::services::database::DatabaseEditor; use flowy_database::services::database::DatabaseEditor;
use flowy_database::services::persistence::database_ref::{DatabaseInfo, DatabaseRef}; use flowy_database::services::persistence::database_ref::{DatabaseInfo, DatabaseViewRef};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub enum DatabaseRefScript { pub enum LinkDatabaseTestScript {
LinkGridToDatabase { CreateGridViewAndLinkToDatabase {
database_id: String, database_id: String,
}, },
#[allow(dead_code)] #[allow(dead_code)]
@ -28,17 +28,17 @@ pub enum DatabaseRefScript {
}, },
} }
pub struct DatabaseRefTest { pub struct LinkDatabaseTest {
inner: DatabaseEditorTest, inner: DatabaseEditorTest,
} }
impl DatabaseRefTest { impl LinkDatabaseTest {
pub async fn new() -> Self { pub async fn new() -> Self {
let inner = DatabaseEditorTest::new_grid().await; let inner = DatabaseEditorTest::new_grid().await;
Self { inner } Self { inner }
} }
pub async fn run_scripts(&mut self, scripts: Vec<DatabaseRefScript>) { pub async fn run_scripts(&mut self, scripts: Vec<LinkDatabaseTestScript>) {
for script in scripts { for script in scripts {
self.run_script(script).await; self.run_script(script).await;
} }
@ -61,7 +61,7 @@ impl DatabaseRefTest {
.unwrap() .unwrap()
} }
pub async fn all_database_ref_views(&self, database_id: &str) -> Vec<DatabaseRef> { pub async fn all_database_ref_views(&self, database_id: &str) -> Vec<DatabaseViewRef> {
self self
.inner .inner
.sdk .sdk
@ -87,9 +87,9 @@ impl DatabaseRefTest {
DatabaseRowTestBuilder::new(self.block_id(view_id).await, field_revs) DatabaseRowTestBuilder::new(self.block_id(view_id).await, field_revs)
} }
pub async fn run_script(&mut self, script: DatabaseRefScript) { pub async fn run_script(&mut self, script: LinkDatabaseTestScript) {
match script { match script {
DatabaseRefScript::LinkGridToDatabase { database_id } => { LinkDatabaseTestScript::CreateGridViewAndLinkToDatabase { database_id } => {
let mut ext = HashMap::new(); let mut ext = HashMap::new();
ext.insert("database_id".to_owned(), database_id); ext.insert("database_id".to_owned(), database_id);
self self
@ -99,7 +99,7 @@ impl DatabaseRefTest {
.create_test_grid_view(&self.inner.app_id, "test link grid", ext) .create_test_grid_view(&self.inner.app_id, "test link grid", ext)
.await; .await;
}, },
DatabaseRefScript::LinkBoardToDatabase { database_id } => { LinkDatabaseTestScript::LinkBoardToDatabase { database_id } => {
let mut ext = HashMap::new(); let mut ext = HashMap::new();
ext.insert("database_id".to_owned(), database_id); ext.insert("database_id".to_owned(), database_id);
self self
@ -109,7 +109,7 @@ impl DatabaseRefTest {
.create_test_board_view(&self.inner.app_id, "test link board", ext) .create_test_board_view(&self.inner.app_id, "test link board", ext)
.await; .await;
}, },
DatabaseRefScript::CreateNewGrid => { LinkDatabaseTestScript::CreateNewGrid => {
self self
.inner .inner
.sdk .sdk
@ -117,15 +117,15 @@ impl DatabaseRefTest {
.create_test_grid_view(&self.inner.app_id, "Create test grid", HashMap::new()) .create_test_grid_view(&self.inner.app_id, "Create test grid", HashMap::new())
.await; .await;
}, },
DatabaseRefScript::AssertNumberOfDatabase { expected } => { LinkDatabaseTestScript::AssertNumberOfDatabase { expected } => {
let databases = self.all_databases().await; let databases = self.all_databases().await;
assert_eq!(databases.len(), expected); assert_eq!(databases.len(), expected);
}, },
DatabaseRefScript::CreateRow { view_id, row_rev } => { LinkDatabaseTestScript::CreateRow { view_id, row_rev } => {
let editor = self.get_database_editor(&view_id).await; let editor = self.get_database_editor(&view_id).await;
let _ = editor.insert_rows(vec![row_rev]).await.unwrap(); let _ = editor.insert_rows(vec![row_rev]).await.unwrap();
}, },
DatabaseRefScript::AssertNumberOfRows { view_id, expected } => { LinkDatabaseTestScript::AssertNumberOfRows { view_id, expected } => {
let editor = self.get_database_editor(&view_id).await; let editor = self.get_database_editor(&view_id).await;
let rows = editor.get_all_row_revs(&view_id).await.unwrap(); let rows = editor.get_all_row_revs(&view_id).await.unwrap();
assert_eq!(rows.len(), expected); assert_eq!(rows.len(), expected);

View File

@ -1,11 +1,12 @@
use crate::database::database_ref_test::script::DatabaseRefScript::*; use crate::database::database_ref_test::script::LinkDatabaseTest;
use crate::database::database_ref_test::script::DatabaseRefTest; use crate::database::database_ref_test::script::LinkDatabaseTestScript::*;
#[tokio::test] #[tokio::test]
async fn database_ref_number_of_database_test() { async fn number_of_database_test() {
let mut test = DatabaseRefTest::new().await; let mut test = LinkDatabaseTest::new().await;
test test
.run_scripts(vec![ .run_scripts(vec![
// After the LinkDatabaseTest initialize, it will create a grid.
AssertNumberOfDatabase { expected: 1 }, AssertNumberOfDatabase { expected: 1 },
CreateNewGrid, CreateNewGrid,
AssertNumberOfDatabase { expected: 2 }, AssertNumberOfDatabase { expected: 2 },
@ -14,12 +15,12 @@ async fn database_ref_number_of_database_test() {
} }
#[tokio::test] #[tokio::test]
async fn database_ref_link_with_existing_database_test() { async fn database_view_link_with_existing_database_test() {
let mut test = DatabaseRefTest::new().await; let mut test = LinkDatabaseTest::new().await;
let database = test.all_databases().await.pop().unwrap(); let database = test.all_databases().await.pop().unwrap();
test test
.run_scripts(vec![ .run_scripts(vec![
LinkGridToDatabase { CreateGridViewAndLinkToDatabase {
database_id: database.database_id, database_id: database.database_id,
}, },
AssertNumberOfDatabase { expected: 1 }, AssertNumberOfDatabase { expected: 1 },
@ -28,36 +29,48 @@ async fn database_ref_link_with_existing_database_test() {
} }
#[tokio::test] #[tokio::test]
async fn database_ref_link_with_existing_database_row_test() { async fn check_number_of_rows_in_linked_database_view() {
let mut test = DatabaseRefTest::new().await; let mut test = LinkDatabaseTest::new().await;
let database = test.all_databases().await.pop().unwrap(); let database = test.all_databases().await.pop().unwrap();
let view = test
.all_database_ref_views(&database.database_id)
.await
.remove(0);
test test
.run_scripts(vec![ .run_scripts(vec![
LinkGridToDatabase { CreateGridViewAndLinkToDatabase {
database_id: database.database_id, database_id: database.database_id,
}, },
AssertNumberOfDatabase { expected: 1 }, // The initial number of rows is 6
AssertNumberOfRows {
view_id: view.view_id.clone(),
expected: 6,
},
]) ])
.await; .await;
} }
#[tokio::test] #[tokio::test]
async fn database_ref_create_new_row_test() { async fn multiple_views_share_database_rows() {
let mut test = DatabaseRefTest::new().await; let mut test = LinkDatabaseTest::new().await;
// After the LinkDatabaseTest initialize, it will create a default database
// with Grid layout.
let database = test.all_databases().await.pop().unwrap(); let database = test.all_databases().await.pop().unwrap();
let database_views = test.all_database_ref_views(&database.database_id).await; let mut database_views = test.all_database_ref_views(&database.database_id).await;
assert_eq!(database_views.len(), 1); assert_eq!(database_views.len(), 1);
let view_id_1 = database_views.get(0).unwrap().view_id.clone(); let view = database_views.remove(0);
test test
.run_scripts(vec![ .run_scripts(vec![
AssertNumberOfRows { AssertNumberOfRows {
view_id: view_id_1.clone(), view_id: view.view_id.clone(),
expected: 6, expected: 6,
}, },
LinkGridToDatabase { CreateGridViewAndLinkToDatabase {
database_id: database.database_id.clone(), database_id: database.database_id.clone(),
}, },
AssertNumberOfDatabase { expected: 1 },
]) ])
.await; .await;
@ -84,6 +97,7 @@ async fn database_ref_create_new_row_test() {
view_id: view_id_2, view_id: view_id_2,
expected: 7, expected: 7,
}, },
AssertNumberOfDatabase { expected: 1 },
]) ])
.await; .await;
} }