diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 0501207968..0d2444c613 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -13,7 +13,7 @@ "preLaunchTask": "AF: Build Appflowy Core", "env": { "RUST_LOG": "trace", - // "RUST_LOG": "debug" + "RUST_BACKTRACE": 1, }, "cwd": "${workspaceRoot}/appflowy_flutter" }, @@ -24,7 +24,7 @@ "program": "./lib/main.dart", "type": "dart", "env": { - "RUST_LOG": "debug" + "RUST_LOG": "debug", }, "cwd": "${workspaceRoot}/appflowy_flutter" }, diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index f9b5e0dd99..b331c85d90 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -59,6 +59,7 @@ BUILD_FLAG = "debug" FLUTTER_OUTPUT_DIR = "Debug" PRODUCT_EXT = "app" BUILD_ARCHS = "arm64" +CRATE_TYPE = "staticlib" [env.development-mac-x86_64] RUST_LOG = "info" @@ -68,6 +69,7 @@ BUILD_FLAG = "debug" FLUTTER_OUTPUT_DIR = "Debug" PRODUCT_EXT = "app" BUILD_ARCHS = "x86_64" +CRATE_TYPE = "staticlib" [env.production-mac-arm64] BUILD_FLAG = "release" @@ -77,6 +79,7 @@ FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "app" APP_ENVIRONMENT = "production" BUILD_ARCHS = "arm64" +CRATE_TYPE = "staticlib" [env.production-mac-x86_64] BUILD_FLAG = "release" @@ -86,6 +89,7 @@ FLUTTER_OUTPUT_DIR = "Release" PRODUCT_EXT = "app" APP_ENVIRONMENT = "production" BUILD_ARCHS = "x86_64" +CRATE_TYPE = "staticlib" [env.development-windows-x86] TARGET_OS = "windows" diff --git a/frontend/appflowy_flutter/lib/core/grid_notification.dart b/frontend/appflowy_flutter/lib/core/grid_notification.dart index d27015cd05..fd2ce352f8 100644 --- a/frontend/appflowy_flutter/lib/core/grid_notification.dart +++ b/frontend/appflowy_flutter/lib/core/grid_notification.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'notification_helper.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart index 9809a8b13d..19eb9d85ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart @@ -13,7 +13,7 @@ class DatabaseCell { /// We use [fieldId + rowId] to identify the cell. class CellCacheKey { final String fieldId; - final String rowId; + final Int64 rowId; CellCacheKey({ required this.fieldId, required this.rowId, @@ -28,7 +28,7 @@ class CellCache { final String viewId; /// fieldId: {cacheKey: GridCell} - final Map> _cellDataByFieldId = {}; + final Map> _cellDataByFieldId = {}; CellCache({ required this.viewId, }); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart index 25799ddaa2..747af37461 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/field/field_listener.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import '../field/field_controller.dart'; import '../field/field_service.dart'; @@ -38,7 +39,7 @@ class CellController extends Equatable { String get viewId => cellId.viewId; - String get rowId => cellId.rowId; + Int64 get rowId => cellId.rowId; String get fieldId => cellId.fieldInfo.id; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart index 42c29dc3e4..dbb65e000a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart @@ -1,7 +1,7 @@ -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'cell_controller.dart'; import 'cell_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart index 89d3e2597c..e126fe4d6f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_listener.dart @@ -1,7 +1,8 @@ import 'package:appflowy/core/grid_notification.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra/notifier.dart'; import 'dart:async'; import 'dart:typed_data'; @@ -9,7 +10,7 @@ import 'dart:typed_data'; typedef UpdateFieldNotifiedValue = Either; class CellListener { - final String rowId; + final Int64 rowId; final String fieldId; PublishNotifier? _updateCellNotifier = PublishNotifier(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart index 19aa0b6048..1b33b840bd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart @@ -1,14 +1,15 @@ import 'dart:async'; import 'dart:collection'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/cell_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:convert' show utf8; @@ -30,7 +31,7 @@ class CellBackendService { ..viewId = cellId.viewId ..fieldId = cellId.fieldId ..rowId = cellId.rowId - ..typeCellData = data; + ..cellChangeset = data; return DatabaseEventUpdateCell(payload).send(); } @@ -51,7 +52,7 @@ class CellBackendService { class CellIdentifier with _$CellIdentifier { const factory CellIdentifier({ required String viewId, - required String rowId, + required Int64 rowId, required FieldInfo fieldInfo, }) = _CellIdentifier; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 6122a37fec..7405f85ed1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -2,17 +2,18 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart'; import 'package:appflowy/plugins/database_view/application/view/view_cache.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:collection/collection.dart'; import 'dart:async'; import 'package:dartz/dartz.dart'; +import 'package:fixnum/fixnum.dart'; import 'database_view_service.dart'; import 'defines.dart'; import 'layout/layout_setting_listener.dart'; @@ -156,7 +157,7 @@ class DatabaseController { } Future> createRow({ - String? startRowId, + Int64? startRowId, String? groupId, void Function(RowDataBuilder builder)? withCells, }) { @@ -198,7 +199,7 @@ class DatabaseController { } Future updateCalenderLayoutSetting( - CalendarLayoutSettingsPB layoutSetting, + CalendarLayoutSettingPB layoutSetting, ) async { await _databaseViewBackendSvc .updateLayoutSetting(calendarLayoutSetting: layoutSetting) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart index b6ca40b630..ebfdbdfd2d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:dartz/dartz.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart index 2b11bd0eea..bb2d468fd0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_view_service.dart @@ -1,14 +1,15 @@ -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:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/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-folder2/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'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:fixnum/fixnum.dart'; class DatabaseViewBackendService { final String viewId; @@ -24,14 +25,12 @@ class DatabaseViewBackendService { } Future> createRow({ - String? startRowId, + Int64? startRowId, String? groupId, Map? cellDataByFieldId, }) { var payload = CreateRowPayloadPB.create()..viewId = viewId; - if (startRowId != null) { - payload.startRowId = startRowId; - } + payload.startRowId = startRowId ?? Int64(0); if (groupId != null) { payload.groupId = groupId; @@ -45,9 +44,9 @@ class DatabaseViewBackendService { } Future> moveRow({ - required String fromRowId, + required Int64 fromRowId, required String toGroupId, - String? toRowId, + Int64? toRowId, }) { var payload = MoveGroupRowPayloadPB.create() ..viewId = viewId @@ -96,17 +95,13 @@ class DatabaseViewBackendService { } Future> updateLayoutSetting({ - CalendarLayoutSettingsPB? calendarLayoutSetting, + CalendarLayoutSettingPB? calendarLayoutSetting, }) { - final layoutSetting = LayoutSettingPB.create(); + final payload = LayoutSettingChangesetPB.create()..viewId = viewId; if (calendarLayoutSetting != null) { - layoutSetting.calendar = calendarLayoutSetting; + payload.calendar = calendarLayoutSetting; } - final payload = UpdateLayoutSettingPB.create() - ..viewId = viewId - ..layoutSetting = layoutSetting; - return DatabaseEventSetLayoutSetting(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index e45a428db7..a91faf878d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -1,7 +1,8 @@ import 'dart:collection'; -import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:fixnum/fixnum.dart'; import '../grid/presentation/widgets/filter/filter_info.dart'; import 'field/field_controller.dart'; @@ -11,12 +12,12 @@ typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnFiltersChanged = void Function(List); typedef OnDatabaseChanged = void Function(DatabasePB); -typedef OnRowsCreated = void Function(List ids); -typedef OnRowsUpdated = void Function(List ids); -typedef OnRowsDeleted = void Function(List ids); +typedef OnRowsCreated = void Function(List ids); +typedef OnRowsUpdated = void Function(List ids); +typedef OnRowsDeleted = void Function(List ids); typedef OnRowsChanged = void Function( UnmodifiableListView rows, - UnmodifiableMapView rowByRowId, + UnmodifiableMapView rowByRowId, RowsChangedReason reason, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart index 493f971177..cdcac04bed 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_action_sheet_bloc.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'field_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart index 5ccb3dff63..2fd72eea99 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_cell_bloc.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart index df2528b96b..bade1b99cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart @@ -1,13 +1,13 @@ import 'dart:collection'; -import 'package:appflowy_backend/protobuf/flowy-database/filter_changeset.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.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/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; import 'package:flutter/foundation.dart'; import '../../grid/presentation/widgets/filter/filter_info.dart'; import '../../grid/presentation/widgets/sort/sort_info.dart'; @@ -94,7 +94,7 @@ class FieldController { _updatedFieldCallbacks = {}; // Group callbacks - final Map _groupConfigurationByFieldId = {}; + final Map _groupConfigurationByFieldId = {}; // Filter callbacks final Map _filterCallbacks = {}; @@ -401,7 +401,7 @@ class FieldController { void _updateSetting(DatabaseViewSettingPB setting) { _groupConfigurationByFieldId.clear(); - for (final configuration in setting.groupConfigurations.items) { + for (final configuration in setting.groupSettings.items) { _groupConfigurationByFieldId[configuration.fieldId] = configuration; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart index 9376cf9d1e..c97f1fa6c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_editor_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dartz/dartz.dart'; import 'field_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart index e26fa398b5..cd8ec286ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_listener.dart @@ -1,11 +1,11 @@ import 'package:appflowy/core/grid_notification.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; import 'package:flowy_infra/notifier.dart'; import 'dart:async'; import 'dart:typed_data'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; typedef UpdateFieldNotifiedValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart index 86f8983d21..91bdad3e88 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_service.dart @@ -1,8 +1,8 @@ -import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_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-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'field_service.freezed.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart index 5f74756652..106d5563ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_type_option_edit_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart index c66681ff76..fd1c69036c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart @@ -1,5 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -41,8 +40,8 @@ class DateTypeOptionBloc } DateTypeOptionPB _updateTypeOption({ - DateFormat? dateFormat, - TimeFormat? timeFormat, + DateFormatPB? dateFormat, + TimeFormatPB? timeFormat, bool? includeTime, }) { state.typeOption.freeze(); @@ -64,9 +63,9 @@ class DateTypeOptionBloc @freezed class DateTypeOptionEvent with _$DateTypeOptionEvent { - const factory DateTypeOptionEvent.didSelectDateFormat(DateFormat format) = + const factory DateTypeOptionEvent.didSelectDateFormat(DateFormatPB format) = _DidSelectDateFormat; - const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormat format) = + const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) = _DidSelectTimeFormat; const factory DateTypeOptionEvent.includeTime(bool includeTime) = _IncludeTime; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart index 873daaeabe..296d3c7b58 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart index 4a6eb5277a..b22696c559 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/multi_select_type_option.dart @@ -1,6 +1,5 @@ import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/multi_select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'dart:async'; import 'select_option_type_option_bloc.dart'; import 'type_option_context.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart index b8e924819f..400dfe3278 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_bloc.dart @@ -1,5 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/format.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/number_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -22,7 +21,7 @@ class NumberTypeOptionBloc ); } - NumberTypeOptionPB _updateNumberFormat(NumberFormat format) { + NumberTypeOptionPB _updateNumberFormat(NumberFormatPB format) { state.typeOption.freeze(); return state.typeOption.rebuild((typeOption) { typeOption.format = format; @@ -32,7 +31,7 @@ class NumberTypeOptionBloc @freezed class NumberTypeOptionEvent with _$NumberTypeOptionEvent { - const factory NumberTypeOptionEvent.didSelectFormat(NumberFormat format) = + const factory NumberTypeOptionEvent.didSelectFormat(NumberFormatPB format) = _DidSelectFormat; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart index e5e6be7b34..b6932ce0ac 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/number_format_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/format.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'number_format_bloc.freezed.dart'; @@ -9,7 +9,8 @@ class NumberFormatBloc extends Bloc { (event, emit) async { event.map( setFilter: (_SetFilter value) { - final List formats = List.from(NumberFormat.values); + final List formats = + List.from(NumberFormatPB.values); if (value.filter.isNotEmpty) { formats.retainWhere( (element) => element @@ -34,92 +35,92 @@ class NumberFormatEvent with _$NumberFormatEvent { @freezed class NumberFormatState with _$NumberFormatState { const factory NumberFormatState({ - required List formats, + required List formats, required String filter, }) = _NumberFormatState; factory NumberFormatState.initial() { return const NumberFormatState( - formats: NumberFormat.values, + formats: NumberFormatPB.values, filter: "", ); } } -extension NumberFormatExtension on NumberFormat { +extension NumberFormatExtension on NumberFormatPB { String title() { switch (this) { - case NumberFormat.ArgentinePeso: + case NumberFormatPB.ArgentinePeso: return "Argentine peso"; - case NumberFormat.Baht: + case NumberFormatPB.Baht: return "Baht"; - case NumberFormat.CanadianDollar: + case NumberFormatPB.CanadianDollar: return "Canadian dollar"; - case NumberFormat.ChileanPeso: + case NumberFormatPB.ChileanPeso: return "Chilean peso"; - case NumberFormat.ColombianPeso: + case NumberFormatPB.ColombianPeso: return "Colombian peso"; - case NumberFormat.DanishKrone: + case NumberFormatPB.DanishKrone: return "Danish krone"; - case NumberFormat.Dirham: + case NumberFormatPB.Dirham: return "Dirham"; - case NumberFormat.EUR: + case NumberFormatPB.EUR: return "Euro"; - case NumberFormat.Forint: + case NumberFormatPB.Forint: return "Forint"; - case NumberFormat.Franc: + case NumberFormatPB.Franc: return "Franc"; - case NumberFormat.HongKongDollar: + case NumberFormatPB.HongKongDollar: return "Hone Kong dollar"; - case NumberFormat.Koruna: + case NumberFormatPB.Koruna: return "Koruna"; - case NumberFormat.Krona: + case NumberFormatPB.Krona: return "Krona"; - case NumberFormat.Leu: + case NumberFormatPB.Leu: return "Leu"; - case NumberFormat.Lira: + case NumberFormatPB.Lira: return "Lira"; - case NumberFormat.MexicanPeso: + case NumberFormatPB.MexicanPeso: return "Mexican peso"; - case NumberFormat.NewTaiwanDollar: + case NumberFormatPB.NewTaiwanDollar: return "New Taiwan dollar"; - case NumberFormat.NewZealandDollar: + case NumberFormatPB.NewZealandDollar: return "New Zealand dollar"; - case NumberFormat.NorwegianKrone: + case NumberFormatPB.NorwegianKrone: return "Norwegian krone"; - case NumberFormat.Num: + case NumberFormatPB.Num: return "Number"; - case NumberFormat.Percent: + case NumberFormatPB.Percent: return "Percent"; - case NumberFormat.PhilippinePeso: + case NumberFormatPB.PhilippinePeso: return "Philippine peso"; - case NumberFormat.Pound: + case NumberFormatPB.Pound: return "Pound"; - case NumberFormat.Rand: + case NumberFormatPB.Rand: return "Rand"; - case NumberFormat.Real: + case NumberFormatPB.Real: return "Real"; - case NumberFormat.Ringgit: + case NumberFormatPB.Ringgit: return "Ringgit"; - case NumberFormat.Riyal: + case NumberFormatPB.Riyal: return "Riyal"; - case NumberFormat.Ruble: + case NumberFormatPB.Ruble: return "Ruble"; - case NumberFormat.Rupee: + case NumberFormatPB.Rupee: return "Rupee"; - case NumberFormat.Rupiah: + case NumberFormatPB.Rupiah: return "Rupiah"; - case NumberFormat.Shekel: + case NumberFormatPB.Shekel: return "Skekel"; - case NumberFormat.USD: + case NumberFormatPB.USD: return "US dollar"; - case NumberFormat.UruguayanPeso: + case NumberFormatPB.UruguayanPeso: return "Uruguayan peso"; - case NumberFormat.Won: + case NumberFormatPB.Won: return "Won"; - case NumberFormat.Yen: + case NumberFormatPB.Yen: return "Yen"; - case NumberFormat.Yuan: + case NumberFormatPB.Yuan: return "Yuan"; default: throw UnimplementedError; @@ -128,13 +129,13 @@ extension NumberFormatExtension on NumberFormat { // String iconName() { // switch (this) { - // case NumberFormat.CNY: + // case NumberFormatPB.CNY: // return "grid/field/yen"; - // case NumberFormat.EUR: + // case NumberFormatPB.EUR: // return "grid/field/euro"; - // case NumberFormat.Number: + // case NumberFormatPB.Number: // return "grid/field/numbers"; - // case NumberFormat.USD: + // case NumberFormatPB.USD: // return "grid/field/us_dollar"; // default: // throw UnimplementedError; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart index 89db8be458..dc5113f469 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart index d0c4d5a44e..9114070f9d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/single_select_type_option.dart @@ -1,6 +1,5 @@ import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/single_select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; import 'select_option_type_option_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart index f9dd370e45..319a63607d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart @@ -1,17 +1,14 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:dartz/dartz.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/multi_select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/number_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/single_select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option.pb.dart'; import 'package:protobuf/protobuf.dart'; - import 'type_option_data_controller.dart'; abstract class TypeOptionParser { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart index eb91dc88a3..4b7eed6188 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_data_controller.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; import 'package:appflowy_backend/log.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart index b440a25af5..79c65ba910 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_service.dart @@ -1,8 +1,8 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.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-database/cell_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; class TypeOptionBackendService { final String viewId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart index fd217e50fb..48cdc04e88 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_listener.dart @@ -3,10 +3,10 @@ import 'dart:typed_data'; import 'package:appflowy/core/grid_notification.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/filter_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart'; import 'package:dartz/dartz.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; typedef UpdateFilterNotifiedValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart index d0de02a9cf..75d77ab49d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart @@ -1,17 +1,17 @@ -import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/number_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; import 'package:fixnum/fixnum.dart' as $fixnum; class FilterBackendService { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart index 1f6fd1efa3..20183d6efb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_listener.dart @@ -3,10 +3,10 @@ import 'dart:typed_data'; import 'package:appflowy/core/grid_notification.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; import 'package:dartz/dartz.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/group_changeset.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart'; typedef GroupUpdateValue = Either; typedef GroupByNewFieldValue = Either, FlowyError>; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart index 52a492201b..9bcfc2edc0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:appflowy/core/grid_notification.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:dartz/dartz.dart'; typedef NewLayoutFieldValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart index b0bb555d51..75058bd188 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/layout_setting_listener.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:appflowy/core/grid_notification.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:dartz/dartz.dart'; typedef LayoutSettingsValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 2bea00fc83..05fa3c913d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -1,7 +1,8 @@ import 'dart:collection'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -42,7 +43,7 @@ class RowCache { return UnmodifiableListView(visibleRows); } - UnmodifiableMapView get rowByRowId { + UnmodifiableMapView get rowByRowId { return UnmodifiableMapView(_rowList.rowInfoByRowId); } @@ -65,7 +66,7 @@ class RowCache { }); } - RowInfo? getRow(String rowId) { + RowInfo? getRow(Int64 rowId) { return _rowList.get(rowId); } @@ -115,7 +116,7 @@ class RowCache { } } - void _deleteRows(List deletedRowIds) { + void _deleteRows(List deletedRowIds) { for (final rowId in deletedRowIds) { final deletedRow = _rowList.remove(rowId); if (deletedRow != null) { @@ -156,7 +157,7 @@ class RowCache { } } - void _hideRows(List invisibleRows) { + void _hideRows(List invisibleRows) { for (final rowId in invisibleRows) { final deletedRow = _rowList.remove(rowId); if (deletedRow != null) { @@ -183,7 +184,7 @@ class RowCache { } RowUpdateCallback addListener({ - required String rowId, + required Int64 rowId, void Function(CellByFieldId, RowsChangedReason)? onCellUpdated, bool Function()? listenWhen, }) { @@ -219,7 +220,7 @@ class RowCache { _rowChangeReasonNotifier.removeListener(callback); } - CellByFieldId loadGridCells(String rowId) { + CellByFieldId loadGridCells(Int64 rowId) { final RowPB? data = _rowList.get(rowId)?.rowPB; if (data == null) { _loadRow(rowId); @@ -227,7 +228,7 @@ class RowCache { return _makeGridCells(rowId, data); } - Future _loadRow(String rowId) async { + Future _loadRow(Int64 rowId) async { final payload = RowIdPB.create() ..viewId = viewId ..rowId = rowId; @@ -239,7 +240,7 @@ class RowCache { ); } - CellByFieldId _makeGridCells(String rowId, RowPB? row) { + CellByFieldId _makeGridCells(Int64 rowId, RowPB? row) { // ignore: prefer_collection_literals var cellDataMap = CellByFieldId(); for (final field in _delegate.fields) { @@ -319,7 +320,7 @@ typedef InsertedIndexs = List; typedef DeletedIndexs = List; // key: id of the row // value: UpdatedIndex -typedef UpdatedIndexMap = LinkedHashMap; +typedef UpdatedIndexMap = LinkedHashMap; @freezed class RowsChangedReason with _$RowsChangedReason { @@ -337,7 +338,7 @@ class RowsChangedReason with _$RowsChangedReason { class InsertedIndex { final int index; - final String rowId; + final Int64 rowId; InsertedIndex({ required this.index, required this.rowId, @@ -355,7 +356,7 @@ class DeletedIndex { class UpdatedIndex { final int index; - final String rowId; + final Int64 rowId; UpdatedIndex({ required this.index, required this.rowId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart index 731cbc5f3d..73b8056078 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart @@ -1,3 +1,4 @@ +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import '../cell/cell_service.dart'; import 'row_cache.dart'; @@ -5,7 +6,7 @@ import 'row_cache.dart'; typedef OnRowChanged = void Function(CellByFieldId, RowsChangedReason); class RowController { - final String rowId; + final Int64 rowId; final String viewId; final List _onRowChangedListeners = []; final RowCache _rowCache; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart index eae593fa64..79d53ec714 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart @@ -1,5 +1,6 @@ import 'dart:collection'; -import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:fixnum/fixnum.dart'; import 'row_cache.dart'; class RowList { @@ -9,13 +10,13 @@ class RowList { List get rows => List.from(_rowInfos); /// Use Map for faster access the raw row data. - final HashMap rowInfoByRowId = HashMap(); + final HashMap rowInfoByRowId = HashMap(); - RowInfo? get(String rowId) { + RowInfo? get(Int64 rowId) { return rowInfoByRowId[rowId]; } - int? indexOfRow(String rowId) { + int? indexOfRow(Int64 rowId) { final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { return _rowInfos.indexOf(rowInfo); @@ -56,7 +57,7 @@ class RowList { } } - DeletedIndex? remove(String rowId) { + DeletedIndex? remove(Int64 rowId) { final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { final index = _rowInfos.indexOf(rowInfo); @@ -145,7 +146,7 @@ class RowList { } } - void moveRow(String rowId, int oldIndex, int newIndex) { + void moveRow(Int64 rowId, int oldIndex, int newIndex) { final index = _rowInfos.indexWhere( (rowInfo) => rowInfo.rowPB.id == rowId, ); @@ -156,7 +157,7 @@ class RowList { } } - bool contains(String rowId) { + bool contains(Int64 rowId) { return rowInfoByRowId[rowId] != null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart index 33e59220df..c4c0d66d21 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_service.dart @@ -1,7 +1,8 @@ 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-database/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:fixnum/fixnum.dart'; class RowBackendService { final String viewId; @@ -10,7 +11,7 @@ class RowBackendService { required this.viewId, }); - Future> createRow(String rowId) { + Future> createRow(Int64 rowId) { final payload = CreateRowPayloadPB.create() ..viewId = viewId ..startRowId = rowId; @@ -18,7 +19,7 @@ class RowBackendService { return DatabaseEventCreateRow(payload).send(); } - Future> getRow(String rowId) { + Future> getRow(Int64 rowId) { final payload = RowIdPB.create() ..viewId = viewId ..rowId = rowId; @@ -26,7 +27,7 @@ class RowBackendService { return DatabaseEventGetRow(payload).send(); } - Future> deleteRow(String rowId) { + Future> deleteRow(Int64 rowId) { final payload = RowIdPB.create() ..viewId = viewId ..rowId = rowId; @@ -34,7 +35,7 @@ class RowBackendService { return DatabaseEventDeleteRow(payload).send(); } - Future> duplicateRow(String rowId) { + Future> duplicateRow(Int64 rowId) { final payload = RowIdPB.create() ..viewId = viewId ..rowId = rowId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart index 1e22879423..2e4a9aa584 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/group_bloc.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/setting/setting_service.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart index 005a9bf967..f1b827df92 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_controller.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; import 'setting_listener.dart'; import 'setting_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart index 6e10238cfb..e6d8fc7627 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_listener.dart @@ -4,8 +4,8 @@ import 'package:appflowy/core/grid_notification.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; typedef UpdateSettingNotifiedValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart index 63ff0449cb..b8505acc3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/setting/setting_service.dart @@ -1,10 +1,10 @@ -import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_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-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/group.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; class SettingBackendService { final String viewId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart index 2ed79bc59c..c78ce733e6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_listener.dart @@ -4,8 +4,8 @@ import 'package:appflowy/core/grid_notification.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:dartz/dartz.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; typedef SortNotifiedValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart index 530dfb11c2..f63c773767 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/sort/sort_service.dart @@ -1,11 +1,11 @@ -import 'package:appflowy_backend/protobuf/flowy-database/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; class SortBackendService { final String viewId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart index 5d8251c82f..7f93054497 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:collection'; import 'package:appflowy_backend/log.dart'; +import 'package:fixnum/fixnum.dart'; import '../defines.dart'; import '../field/field_controller.dart'; import '../row/row_cache.dart'; @@ -39,7 +40,7 @@ class DatabaseViewCache { UnmodifiableListView get rowInfos => _rowCache.rowInfos; RowCache get rowCache => _rowCache; - RowInfo? getRow(String rowId) => _rowCache.getRow(rowId); + RowInfo? getRow(Int64 rowId) => _rowCache.getRow(rowId); DatabaseViewCache({ required this.viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart index ac0d298eb4..699a113edd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_listener.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:appflowy/core/grid_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/view_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart'; typedef RowsVisibilityNotifierValue = Either; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index 1869289d35..e2dfd0d3d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -7,7 +7,8 @@ import 'package:equatable/equatable.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -196,7 +197,7 @@ class BoardBloc extends Bloc { } } - RowCache? getRowCache(String blockId) { + RowCache? getRowCache() { return _databaseController.rowCache; } @@ -310,7 +311,7 @@ class BoardEvent with _$BoardEvent { GroupPB group, RowPB row, ) = _StartEditRow; - const factory BoardEvent.endEditingRow(String rowId) = _EndEditRow; + const factory BoardEvent.endEditingRow(Int64 rowId) = _EndEditRow; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; const factory BoardEvent.didReceiveGridUpdate( DatabasePB grid, @@ -384,7 +385,7 @@ class GroupItem extends AppFlowyGroupItem { } @override - String get id => row.id; + String get id => row.id.toString(); } class GroupControllerDelegateImpl extends GroupControllerDelegate { @@ -422,8 +423,8 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } @override - void removeRow(GroupPB group, String rowId) { - controller.removeGroupItem(group.groupId, rowId); + void removeRow(GroupPB group, Int64 rowId) { + controller.removeGroupItem(group.groupId, rowId.toString()); } @override diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart index 0e6e30eb67..feefa34db7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; class BoardGroupService { final String viewId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart index 6786c58850..ab010920c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/group_controller.dart @@ -1,16 +1,17 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'dart:typed_data'; import 'package:appflowy/core/grid_notification.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra/notifier.dart'; import 'package:dartz/dartz.dart'; typedef OnGroupError = void Function(FlowyError); abstract class GroupControllerDelegate { - void removeRow(GroupPB group, String rowId); + void removeRow(GroupPB group, Int64 rowId); void insertRow(GroupPB group, RowPB row, int? index); void updateRow(GroupPB group, RowPB row); void addNewRow(GroupPB group, RowPB row, int? index); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index bd769b7d21..b6bbebbfee 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -8,8 +8,8 @@ import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; @@ -232,7 +232,7 @@ class _BoardContentState extends State { final groupItem = afGroupItem as GroupItem; final groupData = afGroupData.customData as GroupData; final rowPB = groupItem.row; - final rowCache = context.read().getRowCache(rowPB.blockId); + final rowCache = context.read().getRowCache(); /// Return placeholder widget if the rowCache is null. if (rowCache == null) return SizedBox(key: ObjectKey(groupItem)); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index fb9a4c91f6..d9209e3ca9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -4,9 +4,10 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:dartz/dartz.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -39,7 +40,7 @@ class CalendarBloc extends Bloc { await _openDatabase(emit); _loadAllEvents(); }, - didReceiveCalendarSettings: (CalendarLayoutSettingsPB settings) { + didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) { emit(state.copyWith(settings: Some(settings))); }, didReceiveDatabaseUpdate: (DatabasePB database) { @@ -48,7 +49,7 @@ class CalendarBloc extends Bloc { didLoadAllEvents: (events) { emit(state.copyWith(initialEvents: events, allEvents: events)); }, - didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) { + didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) { _loadAllEvents(); emit(state.copyWith(settings: Some(layoutSettings))); }, @@ -56,7 +57,7 @@ class CalendarBloc extends Bloc { await _createEvent(date, title); }, updateCalendarLayoutSetting: - (CalendarLayoutSettingsPB layoutSetting) async { + (CalendarLayoutSettingPB layoutSetting) async { await _updateCalendarLayoutSetting(layoutSetting); }, didUpdateEvent: (CalendarEventData eventData) { @@ -82,7 +83,7 @@ class CalendarBloc extends Bloc { ), ); }, - didDeleteEvents: (List deletedRowIds) { + didDeleteEvents: (List deletedRowIds) { var events = [...state.allEvents]; events.retainWhere( (element) => !deletedRowIds.contains(element.event!.cellId.rowId), @@ -139,7 +140,7 @@ class CalendarBloc extends Bloc { return state.settings.fold( () => null, (settings) async { - final dateField = _getCalendarFieldInfo(settings.layoutFieldId); + final dateField = _getCalendarFieldInfo(settings.fieldId); final titleField = _getTitleFieldInfo(); if (dateField != null && titleField != null) { final result = await _databaseController.createRow( @@ -159,12 +160,12 @@ class CalendarBloc extends Bloc { } Future _updateCalendarLayoutSetting( - CalendarLayoutSettingsPB layoutSetting, + CalendarLayoutSettingPB layoutSetting, ) async { return _databaseController.updateCalenderLayoutSetting(layoutSetting); } - Future?> _loadEvent(String rowId) async { + Future?> _loadEvent(Int64 rowId) async { final payload = RowIdPB(viewId: viewId, rowId: rowId); return DatabaseEventGetCalendarEvent(payload).send().then((result) { return result.fold( @@ -305,7 +306,7 @@ class CalendarEvent with _$CalendarEvent { // Called after loading the calendar layout setting from the backend const factory CalendarEvent.didReceiveCalendarSettings( - CalendarLayoutSettingsPB settings, + CalendarLayoutSettingPB settings, ) = _ReceiveCalendarSettings; // Called after loading all the current evnets @@ -323,7 +324,7 @@ class CalendarEvent with _$CalendarEvent { ) = _DidReceiveNewEvent; // Called when deleting events - const factory CalendarEvent.didDeleteEvents(List rowIds) = + const factory CalendarEvent.didDeleteEvents(List rowIds) = _DidDeleteEvents; // Called when creating a new event @@ -332,14 +333,14 @@ class CalendarEvent with _$CalendarEvent { // Called when updating the calendar's layout settings const factory CalendarEvent.updateCalendarLayoutSetting( - CalendarLayoutSettingsPB layoutSetting, + CalendarLayoutSettingPB layoutSetting, ) = _UpdateCalendarLayoutSetting; const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = _ReceiveDatabaseUpdate; const factory CalendarEvent.didReceiveNewLayoutField( - CalendarLayoutSettingsPB layoutSettings, + CalendarLayoutSettingPB layoutSettings, ) = _DidReceiveNewLayoutField; } @@ -350,9 +351,9 @@ class CalendarState with _$CalendarState { required Events allEvents, required Events initialEvents, CalendarEventData? newEvent, - required List deleteEventIds, + required List deleteEventIds, CalendarEventData? updateEvent, - required Option settings, + required Option settings, required DatabaseLoadingState loadingState, required Option noneOrError, }) = _CalendarState; @@ -390,6 +391,6 @@ class CalendarDayEvent { final CalendarEventPB event; final CellIdentifier cellId; - String get eventId => cellId.rowId; + Int64 get eventId => cellId.rowId; CalendarDayEvent({required this.cellId, required this.event}); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart index f42c1c049e..f293f72b97 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:bloc/bloc.dart'; import 'package:dartz/dartz.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -9,7 +9,7 @@ typedef DayOfWeek = int; class CalendarSettingBloc extends Bloc { - CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings}) + CalendarSettingBloc({required CalendarLayoutSettingPB? layoutSettings}) : super(CalendarSettingState.initial(layoutSettings)) { on((event, emit) { event.when( @@ -28,11 +28,11 @@ class CalendarSettingBloc class CalendarSettingState with _$CalendarSettingState { const factory CalendarSettingState({ required Option selectedAction, - required Option layoutSetting, + required Option layoutSetting, }) = _CalendarSettingState; factory CalendarSettingState.initial( - CalendarLayoutSettingsPB? layoutSettings, + CalendarLayoutSettingPB? layoutSettings, ) => CalendarSettingState( selectedAction: none(), @@ -46,7 +46,7 @@ class CalendarSettingEvent with _$CalendarSettingEvent { CalendarSettingAction action, ) = _PerformAction; const factory CalendarSettingEvent.updateLayoutSetting( - CalendarLayoutSettingsPB setting, + CalendarLayoutSettingPB setting, ) = _UpdateLayoutSetting; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart index c2b008fd3b..ae137a40cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -4,7 +4,7 @@ import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.da import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart index 85cfe0825f..e2dde2c1a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -6,8 +6,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.da import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart' - hide DateFormat; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; @@ -22,7 +21,7 @@ import 'calendar_setting.dart'; /// calendar class CalendarLayoutSetting extends StatefulWidget { final CalendarSettingContext settingContext; - final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated; + final Function(CalendarLayoutSettingPB? layoutSettings) onUpdated; const CalendarLayoutSetting({ required this.onUpdated, @@ -47,7 +46,7 @@ class _CalendarLayoutSettingState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final CalendarLayoutSettingsPB? settings = state.layoutSetting + final CalendarLayoutSettingPB? settings = state.layoutSetting .foldLeft(null, (previous, settings) => settings); if (settings == null) { @@ -95,7 +94,7 @@ class _CalendarLayoutSettingState extends State { return LayoutDateField( fieldController: widget.settingContext.fieldController, viewId: widget.settingContext.viewId, - fieldId: settings.layoutFieldId, + fieldId: settings.fieldId, popoverMutex: popoverMutex, onUpdated: (fieldId) { _updateLayoutSettings( @@ -128,7 +127,7 @@ class _CalendarLayoutSettingState extends State { } List _availableCalendarSettings( - CalendarLayoutSettingsPB layoutSettings, + CalendarLayoutSettingPB layoutSettings, ) { List settings = [ CalendarLayoutSettingAction.layoutField, @@ -162,13 +161,13 @@ class _CalendarLayoutSettingState extends State { void _updateLayoutSettings( BuildContext context, { - required Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated, + required Function(CalendarLayoutSettingPB? layoutSettings) onUpdated, bool? showWeekends, bool? showWeekNumbers, int? firstDayOfWeek, String? layoutFieldId, }) { - CalendarLayoutSettingsPB setting = context + CalendarLayoutSettingPB setting = context .read() .state .layoutSetting @@ -185,7 +184,7 @@ class _CalendarLayoutSettingState extends State { setting.firstDayOfWeek = firstDayOfWeek; } if (layoutFieldId != null) { - setting.layoutFieldId = layoutFieldId; + setting.fieldId = layoutFieldId; } }); context diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart index a1af44d306..0300bc62c2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; @@ -19,8 +19,8 @@ import 'calendar_layout_setting.dart'; /// contents with the submenu when a category is selected. class CalendarSetting extends StatelessWidget { final CalendarSettingContext settingContext; - final CalendarLayoutSettingsPB? layoutSettings; - final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated; + final CalendarLayoutSettingPB? layoutSettings; + final Function(CalendarLayoutSettingPB? layoutSettings) onUpdated; const CalendarSetting({ required this.onUpdated, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart index 10552297bb..6b94863f45 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checkbox_filter_editor_bloc.dart @@ -1,8 +1,8 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart index 4decd23034..a972911e99 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/checklist_filter_bloc.dart @@ -1,8 +1,8 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart index 966a433361..af2142e165 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart @@ -2,13 +2,13 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_filter.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/number_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart index f88f6a557a..2e459699da 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart @@ -2,8 +2,8 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_listene import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart index 0456537780..6d188c2227 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart index 0e3eedecb8..bac833781e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/text_filter_editor_bloc.dart @@ -1,8 +1,8 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_listener.dart'; import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index 73685670de..26d8f96ae9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -5,7 +5,8 @@ import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../../application/field/field_controller.dart'; @@ -65,7 +66,7 @@ class GridBloc extends Bloc { return super.close(); } - RowCache? getRowCache(String blockId, String rowId) { + RowCache? getRowCache(Int64 rowId) { return databaseController.rowCache; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart index a3df312191..c057486b77 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_header_bloc.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart index 1c8fad5e55..f189972722 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_create_bloc.dart @@ -1,7 +1,7 @@ import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbserver.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart index c01bbc9b6d..bd36dc8dfd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/sort/sort_editor_bloc.dart @@ -1,9 +1,9 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/sort/sort_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbserver.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 86175d7557..8497c89de4 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; @@ -271,7 +271,6 @@ class _GridRowsState extends State<_GridRows> { Animation animation, ) { final rowCache = context.read().getRowCache( - rowInfo.rowPB.blockId, rowInfo.rowPB.id, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart index ea5af95929..e28da08127 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checkbox.dart @@ -5,7 +5,7 @@ import 'package:appflowy_popover/appflowy_popover.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-database/checkbox_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart index 5c43762ed5..83ce71c235 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart @@ -4,7 +4,7 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../condition_button.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart index 6f0ced4c82..e3151f51d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart @@ -3,8 +3,8 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart'; import 'package:flutter/material.dart'; import '../../condition_button.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart index c644454d3a..df40c157b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart @@ -1,10 +1,10 @@ import 'package:appflowy/plugins/database_view/grid/application/filter/select_option_filter_list_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart index f7f121a0c7..392d485ca4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart @@ -1,8 +1,8 @@ import 'package:appflowy/plugins/database_view/grid/application/filter/select_option_filter_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart index e5f64fe839..e4e5a27c88 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import '../../filter_info.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart index 11e547adea..f4b426ca9f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/choicechip/text.dart @@ -4,7 +4,7 @@ import 'package:appflowy_popover/appflowy_popover.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-database/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../application/filter/text_filter_editor_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart index dd1c127d9a..dc18b96cb6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart @@ -1,11 +1,11 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_option_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; class FilterInfo { final String viewId; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart index 26f699bfc9..883de3952c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:flutter/material.dart'; import 'choicechip/checkbox.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart index 400a587f70..93e042373f 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_cell.dart @@ -5,7 +5,7 @@ import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart index 7272338c18..963a7e0245 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; extension FieldTypeListExtension on FieldType { String iconName() { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart index b05d9391c3..b6c4be24e5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart @@ -2,7 +2,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import '../../layout/sizes.dart'; import 'field_type_extension.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart index 0b421a00af..b52d62aa67 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart @@ -7,7 +7,7 @@ import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../layout/sizes.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart index 86d51af982..baa230e7ad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -9,7 +9,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reorderables/reorderables.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart index 879950a59b..8280bf378f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart @@ -3,17 +3,15 @@ import 'dart:typed_data'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checklist_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/multi_select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/number_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/single_select_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option.pb.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'checkbox.dart'; import 'checklist.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart index c2d46b0de1..dadf388203 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart @@ -2,11 +2,11 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/dat import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart' hide DateFormat; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -75,7 +75,10 @@ class DateTypeOptionWidget extends TypeOptionWidget { ); } - Widget _renderDateFormatButton(BuildContext context, DateFormat dataFormat) { + Widget _renderDateFormatButton( + BuildContext context, + DateFormatPB dataFormat, + ) { return AppFlowyPopover( mutex: popoverMutex, asBarrier: true, @@ -102,7 +105,10 @@ class DateTypeOptionWidget extends TypeOptionWidget { ); } - Widget _renderTimeFormatButton(BuildContext context, TimeFormat timeFormat) { + Widget _renderTimeFormatButton( + BuildContext context, + TimeFormatPB timeFormat, + ) { return AppFlowyPopover( mutex: popoverMutex, asBarrier: true, @@ -158,7 +164,7 @@ class DateFormatButton extends StatelessWidget { } class TimeFormatButton extends StatelessWidget { - final TimeFormat timeFormat; + final TimeFormatPB timeFormat; final VoidCallback? onTap; final void Function(bool)? onHover; final EdgeInsets? buttonMargins; @@ -224,8 +230,8 @@ class _IncludeTimeButton extends StatelessWidget { } class DateFormatList extends StatelessWidget { - final DateFormat selectedFormat; - final Function(DateFormat format) onSelected; + final DateFormatPB selectedFormat; + final Function(DateFormatPB format) onSelected; const DateFormatList({ required this.selectedFormat, required this.onSelected, @@ -234,7 +240,7 @@ class DateFormatList extends StatelessWidget { @override Widget build(BuildContext context) { - final cells = DateFormat.values.map((format) { + final cells = DateFormatPB.values.map((format) { return DateFormatCell( dateFormat: format, onSelected: onSelected, @@ -261,8 +267,8 @@ class DateFormatList extends StatelessWidget { class DateFormatCell extends StatelessWidget { final bool isSelected; - final DateFormat dateFormat; - final Function(DateFormat format) onSelected; + final DateFormatPB dateFormat; + final Function(DateFormatPB format) onSelected; const DateFormatCell({ required this.dateFormat, required this.onSelected, @@ -288,18 +294,18 @@ class DateFormatCell extends StatelessWidget { } } -extension DateFormatExtension on DateFormat { +extension DateFormatExtension on DateFormatPB { String title() { switch (this) { - case DateFormat.Friendly: + case DateFormatPB.Friendly: return LocaleKeys.grid_field_dateFormatFriendly.tr(); - case DateFormat.ISO: + case DateFormatPB.ISO: return LocaleKeys.grid_field_dateFormatISO.tr(); - case DateFormat.Local: + case DateFormatPB.Local: return LocaleKeys.grid_field_dateFormatLocal.tr(); - case DateFormat.US: + case DateFormatPB.US: return LocaleKeys.grid_field_dateFormatUS.tr(); - case DateFormat.DayMonthYear: + case DateFormatPB.DayMonthYear: return LocaleKeys.grid_field_dateFormatDayMonthYear.tr(); default: throw UnimplementedError; @@ -308,8 +314,8 @@ extension DateFormatExtension on DateFormat { } class TimeFormatList extends StatelessWidget { - final TimeFormat selectedFormat; - final Function(TimeFormat format) onSelected; + final TimeFormatPB selectedFormat; + final Function(TimeFormatPB format) onSelected; const TimeFormatList({ required this.selectedFormat, required this.onSelected, @@ -318,7 +324,7 @@ class TimeFormatList extends StatelessWidget { @override Widget build(BuildContext context) { - final cells = TimeFormat.values.map((format) { + final cells = TimeFormatPB.values.map((format) { return TimeFormatCell( isSelected: format == selectedFormat, timeFormat: format, @@ -344,9 +350,9 @@ class TimeFormatList extends StatelessWidget { } class TimeFormatCell extends StatelessWidget { - final TimeFormat timeFormat; + final TimeFormatPB timeFormat; final bool isSelected; - final Function(TimeFormat format) onSelected; + final Function(TimeFormatPB format) onSelected; const TimeFormatCell({ required this.timeFormat, required this.onSelected, @@ -372,12 +378,12 @@ class TimeFormatCell extends StatelessWidget { } } -extension TimeFormatExtension on TimeFormat { +extension TimeFormatExtension on TimeFormatPB { String title() { switch (this) { - case TimeFormat.TwelveHour: + case TimeFormatPB.TwelveHour: return LocaleKeys.grid_field_timeFormatTwelveHour.tr(); - case TimeFormat.TwentyFourHour: + case TimeFormatPB.TwentyFourHour: return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(); default: throw UnimplementedError; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart index 3bb2632d17..c6eabc3394 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/number.dart @@ -1,14 +1,14 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/number_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/format.pbenum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:easy_localization/easy_localization.dart' hide NumberFormat; +import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import '../../../layout/sizes.dart'; @@ -117,11 +117,11 @@ class NumberTypeOptionWidget extends TypeOptionWidget { } } -typedef SelectNumberFormatCallback = Function(NumberFormat format); +typedef SelectNumberFormatCallback = Function(NumberFormatPB format); class NumberFormatList extends StatelessWidget { final SelectNumberFormatCallback onSelected; - final NumberFormat selectedFormat; + final NumberFormatPB selectedFormat; const NumberFormatList({ required this.selectedFormat, required this.onSelected, @@ -174,9 +174,9 @@ class NumberFormatList extends StatelessWidget { } class NumberFormatCell extends StatelessWidget { - final NumberFormat format; + final NumberFormatPB format; final bool isSelected; - final Function(NumberFormat format) onSelected; + final Function(NumberFormatPB format) onSelected; const NumberFormatCell({ required this.isSelected, required this.format, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart index ff6a4623e2..7e0647176e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option.dart @@ -1,9 +1,9 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/select_option_type_option_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart index 4e2db973ea..73d90fdd94 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/select_option_editor.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/edit_select_option_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -7,7 +8,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.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/spacing.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart index c4b1c138b0..a61d87ad6c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart index 6673ad9372..920a495371 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_editor.dart @@ -3,7 +3,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_controlle import 'package:appflowy/plugins/database_view/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/sort/util.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart index 0af6acde4d..ff19e8794b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/sort_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; class SortInfo { final SortPB sortPB; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart index 9d710bbe42..d852892bf8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart index 8ee2007ba3..08b72aa430 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/select_option_card_cell_bloc.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart index 870b1d996a..6b3b084d1b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/url_card_cell_bloc.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart index 1227ef8e3c..0926520275 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart index f84e509913..0514514f30 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -1,6 +1,6 @@ import 'dart:collection'; import 'package:equatable/equatable.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index ddf2dee63a..09c9f7b4d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import '../../application/cell/cell_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart index e5942c8ab3..9e2ce6f164 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; typedef CellRenderHook = Widget? Function(C cellData, T cardData); @@ -121,7 +122,7 @@ abstract class EditableCell { class EditableCellId { String fieldId; - String rowId; + Int64 rowId; EditableCellId(this.rowId, this.fieldId); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart index 1a38f727ad..2f5b0450c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index 203da7f26e..c4dd4adae9 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart index 1c90b2583a..8efe354f2f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart index c9b0a5dff4..0c86d39ae0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 1a0a78894c..e9bd57dcfc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -2,13 +2,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -154,10 +153,10 @@ class DateCellCalendarBloc String timeFormatPrompt(FlowyError error) { String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}."; switch (state.dateTypeOptionPB.timeFormat) { - case TimeFormat.TwelveHour: + case TimeFormatPB.TwelveHour: msg = "$msg e.g. 01:00 PM"; break; - case TimeFormat.TwentyFourHour: + case TimeFormatPB.TwentyFourHour: msg = "$msg e.g. 13:00"; break; default: @@ -188,8 +187,8 @@ class DateCellCalendarBloc Future? _updateTypeOption( Emitter emit, { - DateFormat? dateFormat, - TimeFormat? timeFormat, + DateFormatPB? dateFormat, + TimeFormatPB? timeFormat, }) async { state.dateTypeOptionPB.freeze(); final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) { @@ -227,9 +226,9 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent { const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) = _CalendarFormat; const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay; - const factory DateCellCalendarEvent.setTimeFormat(TimeFormat timeFormat) = + const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) = _TimeFormat; - const factory DateCellCalendarEvent.setDateFormat(DateFormat dateFormat) = + const factory DateCellCalendarEvent.setDateFormat(DateFormatPB dateFormat) = _DateFormat; const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = _IncludeTime; @@ -276,9 +275,9 @@ class DateCellCalendarState with _$DateCellCalendarState { String _timeHintText(DateTypeOptionPB typeOption) { switch (typeOption.timeFormat) { - case TimeFormat.TwelveHour: + case TimeFormatPB.TwelveHour: return LocaleKeys.document_date_timeHintTextInTwelveHour.tr(); - case TimeFormat.TwentyFourHour: + case TimeFormatPB.TwentyFourHour: return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr(); default: return ""; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart index 24b6a91de2..475a3c6b6c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index afcc9e2a1f..1bf7505f9b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -3,6 +3,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_ import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:dartz/dartz.dart' show Either; import 'package:easy_localization/easy_localization.dart'; @@ -13,7 +14,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/date_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:table_calendar/table_calendar.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart index 7b8041cb3d..d39ca7d5ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart @@ -1,10 +1,10 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart index 66017e38a9..e63ce1d425 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart index 03332f025f..560f4390a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart index 24c74ed527..a5c34b0967 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart @@ -1,11 +1,11 @@ import 'dart:collection'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart index 46358c5349..68dd2ab55c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'select_option_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart index 0ca1cfac0f..1b84409172 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/select_option_service.dart @@ -1,10 +1,11 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.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-database/cell_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; +import 'package:fixnum/fixnum.dart'; class SelectOptionBackendService { final CellIdentifier cellId; @@ -12,7 +13,7 @@ class SelectOptionBackendService { String get viewId => cellId.viewId; String get fieldId => cellId.fieldInfo.id; - String get rowId => cellId.rowId; + Int64 get rowId => cellId.rowId; Future> create({ required String name, @@ -24,19 +25,17 @@ class SelectOptionBackendService { (result) { return result.fold( (option) { - final cellIdentifier = CellIdPB.create() + final payload = RepeatedSelectOptionPayload.create() ..viewId = viewId ..fieldId = fieldId ..rowId = rowId; - final payload = SelectOptionChangesetPB.create() - ..cellIdentifier = cellIdentifier; if (isSelected) { - payload.insertOptions.add(option); + payload.items.add(option); } else { - payload.updateOptions.add(option); + payload.items.add(option); } - return DatabaseEventUpdateSelectOption(payload).send(); + return DatabaseEventInsertOrUpdateSelectOption(payload).send(); }, (r) => right(r), ); @@ -47,20 +46,24 @@ class SelectOptionBackendService { Future> update({ required SelectOptionPB option, }) { - final payload = SelectOptionChangesetPB.create() - ..updateOptions.add(option) - ..cellIdentifier = _cellIdentifier(); - return DatabaseEventUpdateSelectOption(payload).send(); + final payload = RepeatedSelectOptionPayload.create() + ..items.add(option) + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; + return DatabaseEventInsertOrUpdateSelectOption(payload).send(); } Future> delete({ required Iterable options, }) { - final payload = SelectOptionChangesetPB.create() - ..deleteOptions.addAll(options) - ..cellIdentifier = _cellIdentifier(); + final payload = RepeatedSelectOptionPayload.create() + ..items.addAll(options) + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId; - return DatabaseEventUpdateSelectOption(payload).send(); + return DatabaseEventDeleteSelectOption(payload).send(); } Future> getCellData() { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart index 86c7b1163f..bd8fcfd602 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart @@ -1,7 +1,7 @@ import 'dart:collection'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:flowy_infra/size.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/select_type_option.pb.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart index 68beb7d773..f3eb81160e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart index 70000d6d6a..c57a1c4093 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/url_cell/url_cell_editor_bloc.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/url_type_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index 0e68b3e4c3..39fd0865a0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -9,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; diff --git a/frontend/appflowy_flutter/macos/Runner/Info.plist b/frontend/appflowy_flutter/macos/Runner/Info.plist index c5650a4313..4628404f6e 100644 --- a/frontend/appflowy_flutter/macos/Runner/Info.plist +++ b/frontend/appflowy_flutter/macos/Runner/Info.plist @@ -12,6 +12,13 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleLocalizations + + en + fr + it + zh + CFBundleName $(PRODUCT_NAME) CFBundlePackageType @@ -33,12 +40,5 @@ MainMenu NSPrincipalClass NSApplication - CFBundleLocalizations - - en - fr - it - zh - diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart index a71861f075..69671c2b47 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart @@ -17,8 +17,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; // ignore: unused_import import 'package:protobuf/protobuf.dart'; @@ -29,7 +29,7 @@ import 'error.dart'; part 'dart_event/flowy-folder2/dart_event.dart'; part 'dart_event/flowy-net/dart_event.dart'; part 'dart_event/flowy-user/dart_event.dart'; -part 'dart_event/flowy-database/dart_event.dart'; +part 'dart_event/flowy-database2/dart_event.dart'; part 'dart_event/flowy-document/dart_event.dart'; part 'dart_event/flowy-document2/dart_event.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart index a5e4a13eaa..dfc3a072b5 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart index da8aebc73f..74af637a60 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart index ffb55a5957..3e6cc400c2 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_ import 'package:appflowy/plugins/database_view/application/setting/group_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart index 14a3a58c65..adede9d1e1 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:bloc_test/bloc_test.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index da0722bb04..43a7c05702 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -10,9 +10,9 @@ import 'package:appflowy/plugins/database_view/board/board.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/workspace/application/app/app_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import '../../util.dart'; import '../grid_test/util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart index 15626fb7f8..73a43f67d0 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option.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/select_type_option.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart index 0423672763..2d23614c07 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart index 060ba80a23..f68506030b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart @@ -1,9 +1,9 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart index d88859bc0e..6c949c3985 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/edit_filter_field_test.dart @@ -2,8 +2,8 @@ import 'package:appflowy/plugins/database_view/application/field/field_editor_bl import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart index 6123bff918..9f3cf6ff2d 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart index 1d2bd20ad9..a56c70f326 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/checkbox_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart index 51557b91d9..f28e27a5b9 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/filter/filter_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart index 9943428360..85e7d53d24 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart @@ -1,7 +1,7 @@ import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/grid/grid.dart'; import 'package:appflowy/workspace/application/app/app_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import '../util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart index da3a569638..c2242bb14f 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:bloc_test/bloc_test.dart'; import 'util.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index f211804f9d..f54ecab661 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -10,11 +10,11 @@ import 'package:appflowy/plugins/database_view/application/database_controller.d import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/grid.dart'; import 'package:appflowy/workspace/application/app/app_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/setting_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:dartz/dartz.dart'; import '../../util.dart'; diff --git a/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart b/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart index 0ae2ac2f3f..c1fb2eb6d9 100644 --- a/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; -import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:textfield_tags/textfield_tags.dart'; diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 16c7d33408..f4085cce35 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -640,7 +640,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "anyhow", "bytes", @@ -655,10 +655,32 @@ dependencies = [ "yrs", ] +[[package]] +name = "collab-database" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-derive", + "collab-persistence", + "lazy_static", + "lru", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "proc-macro2", "quote", @@ -670,7 +692,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "anyhow", "collab", @@ -687,7 +709,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "anyhow", "collab", @@ -705,7 +727,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "bincode", "chrono", @@ -1403,11 +1425,10 @@ dependencies = [ "collab-persistence", "database-model", "flowy-client-ws", - "flowy-database", + "flowy-database2", "flowy-document", "flowy-document2", "flowy-error", - "flowy-folder", "flowy-folder2", "flowy-net", "flowy-revision", @@ -1430,40 +1451,33 @@ dependencies = [ ] [[package]] -name = "flowy-database" +name = "flowy-database2" version = "0.1.0" dependencies = [ "anyhow", "async-stream", - "atomic_refcell", "bytes", "chrono", - "crossbeam-utils", + "collab", + "collab-database", + "collab-persistence", "dashmap", "database-model", - "diesel", "fancy-regex 0.10.0", - "flowy-client-sync", "flowy-codegen", "flowy-derive", "flowy-error", "flowy-notification", - "flowy-revision", - "flowy-revision-persistence", - "flowy-sqlite", "flowy-task", "futures", "indexmap", "lazy_static", "lib-dispatch", "lib-infra", - "lib-ot", "nanoid", "parking_lot 0.12.1", "protobuf", "rayon", - "regex", - "revision-model", "rust_decimal", "rusty-money", "serde", @@ -1559,6 +1573,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "collab-database", "flowy-client-sync", "flowy-client-ws", "flowy-codegen", @@ -1577,43 +1592,6 @@ dependencies = [ "user-model", ] -[[package]] -name = "flowy-folder" -version = "0.1.0" -dependencies = [ - "bytes", - "diesel", - "diesel_derives", - "flowy-client-sync", - "flowy-codegen", - "flowy-derive", - "flowy-document", - "flowy-error", - "flowy-notification", - "flowy-revision", - "flowy-revision-persistence", - "flowy-sqlite", - "folder-model", - "futures", - "lazy_static", - "lib-dispatch", - "lib-infra", - "lib-ot", - "log", - "parking_lot 0.12.1", - "pin-project", - "protobuf", - "revision-model", - "serde", - "serde_json", - "strum", - "strum_macros", - "tokio", - "tracing", - "unicode-segmentation", - "ws-model", -] - [[package]] name = "flowy-folder2" version = "0.1.0" @@ -2883,7 +2861,6 @@ dependencies = [ "glob", "libc", "libz-sys", - "lz4-sys", "zstd-sys", ] @@ -2974,13 +2951,12 @@ dependencies = [ ] [[package]] -name = "lz4-sys" -version = "1.9.4" +name = "lru" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" dependencies = [ - "cc", - "libc", + "hashbrown 0.13.2", ] [[package]] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 955d708e68..e9b69b7df7 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -33,15 +33,17 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } #collab = { path = "../../AppFlowy-Collab/collab" } #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } #collab-persistence = { path = "../../AppFlowy-Collab/collab-persistence" } #collab-document = { path = "../../AppFlowy-Collab/collab-document" } +#collab-database= { path = "../../AppFlowy-Collab/collab-database" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 4014bbaaf0..fb0551c572 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -544,7 +544,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "anyhow", "bytes", @@ -559,10 +559,32 @@ dependencies = [ "yrs", ] +[[package]] +name = "collab-database" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-derive", + "collab-persistence", + "lazy_static", + "lru", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "proc-macro2", "quote", @@ -574,7 +596,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "anyhow", "collab", @@ -591,7 +613,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "anyhow", "collab", @@ -609,7 +631,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=fd64f5#fd64f5b9ccc40aa52eae34789133f39172259832" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=c5aba2#c5aba21c9652e76a0960ab1add329266d6e0e6e7" dependencies = [ "bincode", "chrono", @@ -1268,11 +1290,10 @@ dependencies = [ "console-subscriber", "database-model", "flowy-client-ws", - "flowy-database", + "flowy-database2", "flowy-document", "flowy-document2", "flowy-error", - "flowy-folder", "flowy-folder2", "flowy-net", "flowy-revision", @@ -1310,7 +1331,6 @@ dependencies = [ "fancy-regex 0.10.0", "flowy-client-sync", "flowy-codegen", - "flowy-database", "flowy-derive", "flowy-error", "flowy-notification", @@ -1343,6 +1363,47 @@ dependencies = [ "url", ] +[[package]] +name = "flowy-database2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "bytes", + "chrono", + "collab", + "collab-database", + "collab-persistence", + "dashmap", + "database-model", + "fancy-regex 0.10.0", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-task", + "flowy-test", + "futures", + "indexmap", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "rayon", + "rust_decimal", + "rusty-money", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros", + "tokio", + "tracing", + "url", +] + [[package]] name = "flowy-derive" version = "0.1.0" @@ -1439,6 +1500,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "collab-database", "flowy-client-sync", "flowy-client-ws", "flowy-codegen", @@ -1457,45 +1519,6 @@ dependencies = [ "user-model", ] -[[package]] -name = "flowy-folder" -version = "0.1.0" -dependencies = [ - "bytes", - "diesel", - "diesel_derives", - "flowy-client-sync", - "flowy-codegen", - "flowy-derive", - "flowy-document", - "flowy-error", - "flowy-folder", - "flowy-notification", - "flowy-revision", - "flowy-revision-persistence", - "flowy-sqlite", - "flowy-test", - "folder-model", - "futures", - "lazy_static", - "lib-dispatch", - "lib-infra", - "lib-ot", - "log", - "parking_lot 0.12.1", - "pin-project", - "protobuf", - "revision-model", - "serde", - "serde_json", - "strum", - "strum_macros", - "tokio", - "tracing", - "unicode-segmentation", - "ws-model", -] - [[package]] name = "flowy-folder2" version = "0.1.0" @@ -2484,7 +2507,6 @@ dependencies = [ "glob", "libc", "libz-sys", - "lz4-sys", "zstd-sys", ] @@ -2551,13 +2573,12 @@ dependencies = [ ] [[package]] -name = "lz4-sys" -version = "1.9.4" +name = "lru" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" dependencies = [ - "cc", - "libc", + "hashbrown 0.13.2", ] [[package]] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 9b005fe15b..950116c8d8 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -17,6 +17,7 @@ members = [ "flowy-revision", "flowy-revision-persistence", "flowy-database", + "flowy-database2", "flowy-task", "flowy-client-sync", "flowy-derive", @@ -39,12 +40,14 @@ opt-level = 3 incremental = false [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "fd64f5" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "c5aba2" } -# collab = { path = "../AppFlowy-Collab/collab" } -# collab-folder = { path = "../AppFlowy-Collab/collab-folder" } -# collab-persistence = { path = "../AppFlowy-Collab/collab-persistence" } -# collab-document = { path = "../AppFlowy-Collab/collab-document" } \ No newline at end of file +#collab = { path = "../AppFlowy-Collab/collab" } +#collab-folder = { path = "../AppFlowy-Collab/collab-folder" } +#collab-database= { path = "../AppFlowy-Collab/collab-database" } +#collab-persistence = { path = "../AppFlowy-Collab/collab-persistence" } +#collab-document = { path = "../AppFlowy-Collab/collab-document" } diff --git a/frontend/rust-lib/dart-ffi/.cargo/config.toml b/frontend/rust-lib/dart-ffi/.cargo/config.toml index bff29e6e17..2431421f7e 100644 --- a/frontend/rust-lib/dart-ffi/.cargo/config.toml +++ b/frontend/rust-lib/dart-ffi/.cargo/config.toml @@ -1,2 +1,5 @@ [build] rustflags = ["--cfg", "tokio_unstable"] + +#[target.aarch64-apple-darwin] +#BINDGEN_EXTRA_CLANG_ARGS="--target=aarch64-apple-darwin" \ No newline at end of file diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index ee11f4d4f0..fdf2531b43 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -10,9 +10,9 @@ lib-dispatch = { path = "../lib-dispatch" } lib-log = { path = "../lib-log" } flowy-user = { path = "../flowy-user" } flowy-net = { path = "../flowy-net" } -flowy-folder = { path = "../flowy-folder" } flowy-folder2 = { path = "../flowy-folder2" } -flowy-database = { path = "../flowy-database" } +#flowy-database = { path = "../flowy-database" } +flowy-database2 = { path = "../flowy-database2" } database-model = { path = "../../../shared-lib/database-model" } user-model = { path = "../../../shared-lib/user-model" } flowy-client-ws = { path = "../../../shared-lib/flowy-client-ws" } @@ -41,32 +41,28 @@ serde_json = "1.0" [features] default = ["rev-sqlite"] profiling = ["console-subscriber", "tokio/tracing"] -http_sync = ["flowy-folder/cloud_sync", "flowy-document/cloud_sync"] -native_sync = ["flowy-folder/cloud_sync", "flowy-document/cloud_sync"] +http_sync = ["flowy-document/cloud_sync"] +native_sync = ["flowy-document/cloud_sync"] use_bunyan = ["lib-log/use_bunyan"] dart = [ "flowy-user/dart", "flowy-net/dart", -# "flowy-folder/dart", "flowy-folder2/dart", - "flowy-database/dart", + "flowy-database2/dart", "flowy-document/dart", "flowy-document2/dart", ] ts = [ "flowy-user/ts", "flowy-net/ts", -# "flowy-folder/ts", "flowy-folder2/ts", - "flowy-database/ts", + "flowy-database2/ts", "flowy-document/ts", "flowy-document2/ts", ] rev-sqlite = [ "flowy-sqlite", "flowy-user/rev-sqlite", - "flowy-folder/rev-sqlite", - "flowy-database/rev-sqlite", "flowy-document/rev-sqlite", ] openssl_vendored = ["flowy-sqlite/openssl_vendored"] diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs new file mode 100644 index 0000000000..a294db66f6 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use collab_persistence::kv::rocks_kv::RocksCollabDB; +use tokio::sync::RwLock; + +use flowy_client_ws::FlowyWebSocketConnect; +use flowy_database2::{DatabaseManager2, DatabaseUser2}; +use flowy_error::FlowyError; +use flowy_task::TaskDispatcher; +use flowy_user::services::UserSession; + +pub struct Database2DepsResolver(); + +impl Database2DepsResolver { + pub async fn resolve( + _ws_conn: Arc, + user_session: Arc, + task_scheduler: Arc>, + ) -> Arc { + let user = Arc::new(DatabaseUserImpl(user_session)); + Arc::new(DatabaseManager2::new(user, task_scheduler)) + } +} + +struct DatabaseUserImpl(Arc); +impl DatabaseUser2 for DatabaseUserImpl { + fn user_id(&self) -> Result { + self + .0 + .user_id() + .map_err(|e| FlowyError::internal().context(e)) + } + + fn token(&self) -> Result { + self + .0 + .token() + .map_err(|e| FlowyError::internal().context(e)) + } + + fn kv_db(&self) -> Result, FlowyError> { + self.0.get_kv_db() + } +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs index 9cc43b3f0e..0ae076564a 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use collab_persistence::kv::rocks_kv::RocksCollabDB; -use flowy_database::manager::DatabaseManager; + +use flowy_database2::DatabaseManager2; use flowy_document2::manager::{DocumentManager as DocumentManager2, DocumentUser}; use flowy_error::FlowyError; use flowy_user::services::UserSession; @@ -10,9 +11,9 @@ pub struct Document2DepsResolver(); impl Document2DepsResolver { pub fn resolve( user_session: Arc, - _database_manager: &Arc, + _database_manager: &Arc, ) -> Arc { - let user: Arc = Arc::new(DocumentUserImpl(user_session.clone())); + let user: Arc = Arc::new(DocumentUserImpl(user_session)); Arc::new(DocumentManager2::new(user.clone())) } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs index 7f4987a09f..dc16790f39 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs @@ -1,13 +1,14 @@ +use std::collections::HashMap; +use std::sync::Arc; + use bytes::Bytes; use collab_persistence::kv::rocks_kv::RocksCollabDB; -use database_model::BuildDatabaseContext; -use flowy_database::entities::DatabaseLayoutPB; -use flowy_database::manager::{create_new_database, link_existing_database, DatabaseManager}; -use flowy_database::util::{make_default_board, make_default_calendar, make_default_grid}; +use flowy_database2::entities::DatabaseLayoutPB; +use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid}; +use flowy_database2::DatabaseManager2; use flowy_document::editor::make_transaction_from_document_content; use flowy_document::DocumentManager; use flowy_error::FlowyError; - use flowy_folder2::entities::ViewLayoutPB; use flowy_folder2::manager::{Folder2Manager, FolderUser}; use flowy_folder2::view_ext::{ViewDataProcessor, ViewDataProcessorMap}; @@ -15,16 +16,13 @@ use flowy_folder2::ViewLayout; use flowy_user::services::UserSession; use lib_infra::future::FutureResult; use revision_model::Revision; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::sync::Arc; pub struct Folder2DepsResolver(); impl Folder2DepsResolver { pub async fn resolve( user_session: Arc, document_manager: &Arc, - database_manager: &Arc, + database_manager: &Arc, ) -> Arc { let user: Arc = Arc::new(FolderUserImpl(user_session.clone())); @@ -40,7 +38,7 @@ impl Folder2DepsResolver { fn make_view_data_processor( document_manager: Arc, - database_manager: Arc, + database_manager: Arc, ) -> ViewDataProcessorMap { let mut map: HashMap> = HashMap::new(); @@ -145,7 +143,7 @@ impl ViewDataProcessor for DocumentViewDataProcessor { } } -struct DatabaseViewDataProcessor(Arc); +struct DatabaseViewDataProcessor(Arc); impl ViewDataProcessor for DatabaseViewDataProcessor { fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { let database_manager = self.0.clone(); @@ -160,9 +158,8 @@ impl ViewDataProcessor for DatabaseViewDataProcessor { let database_manager = self.0.clone(); let view_id = view_id.to_owned(); FutureResult::new(async move { - let editor = database_manager.open_database_view(&view_id).await?; - let delta_bytes = editor.duplicate_database(&view_id).await?; - Ok(delta_bytes.into()) + let delta_bytes = database_manager.duplicate_database(&view_id).await?; + Ok(Bytes::from(delta_bytes)) }) } @@ -176,40 +173,29 @@ impl ViewDataProcessor for DatabaseViewDataProcessor { view_id: &str, name: &str, layout: ViewLayout, - ext: HashMap, + _ext: HashMap, ) -> FutureResult<(), FlowyError> { - let view_id = view_id.to_string(); let name = name.to_string(); let database_manager = self.0.clone(); - match DatabaseExtParams::from_map(ext).map(|params| params.database_id) { - None => { - let (build_context, layout) = match layout { - ViewLayout::Grid => (make_default_grid(), DatabaseLayoutPB::Grid), - ViewLayout::Board => (make_default_board(), DatabaseLayoutPB::Board), - ViewLayout::Calendar => (make_default_calendar(), DatabaseLayoutPB::Calendar), - ViewLayout::Document => { - return FutureResult::new(async move { - Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout))) - }); - }, - }; - FutureResult::new(async move { - create_new_database(&view_id, name, layout, database_manager, build_context).await - }) + let data = match layout { + ViewLayout::Grid => make_default_grid(view_id, &name), + ViewLayout::Board => make_default_board(view_id, &name), + ViewLayout::Calendar => make_default_calendar(view_id, &name), + ViewLayout::Document => { + return FutureResult::new(async move { + Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout))) + }); }, - Some(database_id) => { - let layout = layout_type_from_view_layout(layout.into()); - FutureResult::new(async move { - link_existing_database(&view_id, name, &database_id, layout, database_manager).await - }) - }, - } + }; + FutureResult::new(async move { + database_manager.create_database_with_params(data).await?; + Ok(()) + }) } - /// Create a database view with custom data. + /// Create a database view with duplicated data. /// If the ext contains the {"database_id": "xx"}, then it will link - /// to the existing database. The data of the database will be shared - /// within these references views. + /// to the existing database. fn create_view_with_custom_data( &self, _user_id: i64, @@ -219,30 +205,47 @@ impl ViewDataProcessor for DatabaseViewDataProcessor { layout: ViewLayout, ext: HashMap, ) -> FutureResult<(), FlowyError> { - let view_id = view_id.to_string(); - let database_manager = self.0.clone(); - let layout = layout_type_from_view_layout(layout.into()); - let name = name.to_string(); - match DatabaseExtParams::from_map(ext).map(|params| params.database_id) { - None => FutureResult::new(async move { - let bytes = Bytes::from(data); - let build_context = BuildDatabaseContext::try_from(bytes)?; - let _ = create_new_database(&view_id, name, layout, database_manager, build_context).await; - Ok(()) - }), - Some(database_id) => FutureResult::new(async move { - link_existing_database(&view_id, name, &database_id, layout, database_manager).await - }), + match CreateDatabaseExtParams::from_map(ext) { + None => { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + database_manager + .create_database_with_database_data(&view_id, data) + .await?; + Ok(()) + }) + }, + Some(params) => { + let database_manager = self.0.clone(); + let layout = layout_type_from_view_layout(layout.into()); + let name = name.to_string(); + let target_view_id = view_id.to_string(); + + FutureResult::new(async move { + database_manager + .create_linked_view( + name, + layout, + params.database_id, + target_view_id, + params.duplicated_view_id, + ) + .await?; + Ok(()) + }) + }, } } } #[derive(Debug, serde::Deserialize)] -struct DatabaseExtParams { +struct CreateDatabaseExtParams { database_id: String, + duplicated_view_id: Option, } -impl DatabaseExtParams { +impl CreateDatabaseExtParams { pub fn from_map(map: HashMap) -> Option { let value = serde_json::to_value(map).ok()?; serde_json::from_value::(value).ok() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs deleted file mode 100644 index 77f07ebde3..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/grid_deps.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::FlowyError; -use bytes::Bytes; -use flowy_client_ws::FlowyWebSocketConnect; -use flowy_database::manager::{DatabaseManager, DatabaseUser}; -use flowy_database::services::persistence::DatabaseDBConnection; -use flowy_revision::{RevisionWebSocket, WSStateReceiver}; -use flowy_sqlite::ConnectionPool; -use flowy_task::TaskDispatcher; -use flowy_user::services::UserSession; -use futures_core::future::BoxFuture; -use lib_infra::future::BoxResultFuture; -use lib_ws::{WSChannel, WebSocketRawMessage}; -use std::convert::TryInto; -use std::sync::Arc; -use tokio::sync::RwLock; -use ws_model::ws_revision::ClientRevisionWSData; - -pub struct DatabaseDepsResolver(); - -impl DatabaseDepsResolver { - pub async fn resolve( - ws_conn: Arc, - user_session: Arc, - task_scheduler: Arc>, - ) -> Arc { - let user = Arc::new(GridUserImpl(user_session.clone())); - let rev_web_socket = Arc::new(GridRevisionWebSocket(ws_conn)); - Arc::new(DatabaseManager::new( - user, - rev_web_socket, - task_scheduler, - Arc::new(DatabaseDBConnectionImpl(user_session)), - )) - } -} - -struct DatabaseDBConnectionImpl(Arc); -impl DatabaseDBConnection for DatabaseDBConnectionImpl { - fn get_db_pool(&self) -> Result, FlowyError> { - self - .0 - .db_pool() - .map_err(|e| FlowyError::internal().context(e)) - } -} - -struct GridUserImpl(Arc); -impl DatabaseUser for GridUserImpl { - fn user_id(&self) -> Result { - self.0.user_id() - } - - fn token(&self) -> Result { - self.0.token() - } - - fn db_pool(&self) -> Result, FlowyError> { - self.0.db_pool() - } -} - -struct GridRevisionWebSocket(Arc); -impl RevisionWebSocket for GridRevisionWebSocket { - fn send(&self, data: ClientRevisionWSData) -> BoxResultFuture<(), FlowyError> { - let bytes: Bytes = data.try_into().unwrap(); - let msg = WebSocketRawMessage { - channel: WSChannel::Database, - data: bytes.to_vec(), - }; - - let ws_conn = self.0.clone(); - Box::pin(async move { - match ws_conn.web_socket().await? { - None => {}, - Some(sender) => { - sender - .send(msg) - .map_err(|e| FlowyError::internal().context(e))?; - }, - } - Ok(()) - }) - } - - fn subscribe_state_changed(&self) -> BoxFuture { - let ws_conn = self.0.clone(); - Box::pin(async move { ws_conn.subscribe_websocket_state().await }) - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs index 6603edb598..6682961e31 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs @@ -1,12 +1,13 @@ +pub use database_deps::*; mod document2_deps; mod document_deps; mod folder2_deps; -mod grid_deps; mod user_deps; mod util; pub use document2_deps::*; pub use document_deps::*; pub use folder2_deps::*; -pub use grid_deps::*; pub use user_deps::*; + +mod database_deps; diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index cb42420c8f..54191ab542 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,15 +1,20 @@ -mod deps_resolve; -pub mod module; -use crate::deps_resolve::*; +use std::time::Duration; +use std::{ + fmt, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +use tokio::sync::{broadcast, RwLock}; use flowy_client_ws::{listen_on_websocket, FlowyWebSocketConnect, NetworkType}; -use flowy_database::entities::DatabaseLayoutPB; -use flowy_database::manager::DatabaseManager; +use flowy_database2::DatabaseManager2; use flowy_document::entities::DocumentVersionPB; use flowy_document::{DocumentConfig, DocumentManager}; use flowy_document2::manager::DocumentManager as DocumentManager2; use flowy_error::FlowyResult; -use flowy_folder::errors::FlowyError; use flowy_folder2::manager::Folder2Manager; pub use flowy_net::get_client_server_configuration; use flowy_net::local_server::LocalServer; @@ -22,17 +27,13 @@ use lib_dispatch::runtime::tokio_default_runtime; use lib_infra::future::{to_fut, Fut}; use module::make_plugins; pub use module::*; -use std::time::Duration; -use std::{ - fmt, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; -use tokio::sync::{broadcast, RwLock}; use user_model::UserProfile; +use crate::deps_resolve::*; + +mod deps_resolve; +pub mod module; + static INIT_LOG: AtomicBool = AtomicBool::new(false); /// This name will be used as to identify the current [AppFlowyCore] instance. @@ -93,11 +94,13 @@ fn create_log_filter(level: String, with_crates: Vec) -> String { filters.push(format!("flowy_folder2={}", level)); filters.push(format!("collab_folder={}", level)); filters.push(format!("collab_persistence={}", level)); + filters.push(format!("collab_database={}", level)); filters.push(format!("collab={}", level)); filters.push(format!("flowy_user={}", level)); filters.push(format!("flowy_document={}", level)); filters.push(format!("flowy_document2={}", level)); filters.push(format!("flowy_database={}", level)); + filters.push(format!("flowy_database2={}", level)); filters.push(format!("flowy_sync={}", "info")); filters.push(format!("flowy_client_sync={}", "info")); filters.push(format!("flowy_notification={}", "info")); @@ -130,7 +133,8 @@ pub struct AppFlowyCore { pub document_manager: Arc, pub document_manager2: Arc, pub folder_manager: Arc, - pub database_manager: Arc, + // pub database_manager: Arc, + pub database_manager: Arc, pub event_dispatcher: Arc, pub ws_conn: Arc, pub local_server: Option>, @@ -167,8 +171,7 @@ impl AppFlowyCore { &config.server_config, &config.document, ); - - let database_manager = DatabaseDepsResolver::resolve( + let database_manager2 = Database2DepsResolver::resolve( ws_conn.clone(), user_session.clone(), task_dispatcher.clone(), @@ -176,11 +179,11 @@ impl AppFlowyCore { .await; let folder_manager = - Folder2DepsResolver::resolve(user_session.clone(), &document_manager, &database_manager) + Folder2DepsResolver::resolve(user_session.clone(), &document_manager, &database_manager2) .await; let document_manager2 = - Document2DepsResolver::resolve(user_session.clone(), &database_manager); + Document2DepsResolver::resolve(user_session.clone(), &database_manager2); if let Some(local_server) = local_server.as_ref() { local_server.run(); @@ -191,7 +194,7 @@ impl AppFlowyCore { document_manager, folder_manager, local_server, - database_manager, + database_manager2, document_manager2, ) }); @@ -312,7 +315,7 @@ fn mk_user_session( struct UserStatusListener { document_manager: Arc, folder_manager: Arc, - database_manager: Arc, + database_manager: Arc, ws_conn: Arc, #[allow(dead_code)] config: AppFlowyCoreConfig, @@ -322,27 +325,7 @@ impl UserStatusListener { async fn did_sign_in(&self, token: &str, user_id: i64) -> FlowyResult<()> { self.folder_manager.initialize(user_id).await?; self.document_manager.initialize(user_id).await?; - let cloned_folder_manager = self.folder_manager.clone(); - let get_views_fn = to_fut(async move { - cloned_folder_manager - .get_current_workspace_views() - .await - .unwrap_or_default() - .into_iter() - .filter(|view| view.layout.is_database()) - .map(|view| { - ( - view.id, - view.name, - layout_type_from_view_layout(view.layout), - ) - }) - .collect::>() - }); - self - .database_manager - .initialize(user_id, token, get_views_fn) - .await?; + self.database_manager.initialize(user_id, token).await?; self .ws_conn .start(token.to_owned(), user_id.to_owned()) diff --git a/frontend/rust-lib/flowy-core/src/module.rs b/frontend/rust-lib/flowy-core/src/module.rs index 2b55edb7f5..c2f557b6ab 100644 --- a/frontend/rust-lib/flowy-core/src/module.rs +++ b/frontend/rust-lib/flowy-core/src/module.rs @@ -1,5 +1,5 @@ use flowy_client_ws::FlowyWebSocketConnect; -use flowy_database::manager::DatabaseManager; +use flowy_database2::DatabaseManager2; use flowy_document::DocumentManager; use flowy_document2::manager::DocumentManager as DocumentManager2; @@ -11,7 +11,7 @@ use std::sync::Arc; pub fn make_plugins( ws_conn: &Arc, folder_manager: &Arc, - grid_manager: &Arc, + database_manager: &Arc, user_session: &Arc, document_manager: &Arc, document_manager2: &Arc, @@ -19,14 +19,14 @@ pub fn make_plugins( let user_plugin = flowy_user::event_map::init(user_session.clone()); let folder_plugin = flowy_folder2::event_map::init(folder_manager.clone()); let network_plugin = flowy_net::event_map::init(ws_conn.clone()); - let grid_plugin = flowy_database::event_map::init(grid_manager.clone()); + let database_plugin = flowy_database2::event_map::init(database_manager.clone()); let document_plugin = flowy_document::event_map::init(document_manager.clone()); let document_plugin2 = flowy_document2::event_map::init(document_manager2.clone()); vec![ user_plugin, folder_plugin, network_plugin, - grid_plugin, + database_plugin, document_plugin, document_plugin2, ] diff --git a/frontend/rust-lib/flowy-database/Cargo.toml b/frontend/rust-lib/flowy-database/Cargo.toml index ec686c098e..904035380a 100644 --- a/frontend/rust-lib/flowy-database/Cargo.toml +++ b/frontend/rust-lib/flowy-database/Cargo.toml @@ -50,7 +50,7 @@ parking_lot = "0.12.1" [dev-dependencies] flowy-test = { path = "../flowy-test" } -flowy-database = { path = "", features = ["flowy_unit_test"]} +#flowy-database = { path = "", features = ["flowy_unit_test"]} [build-dependencies] flowy-codegen = { path = "../flowy-codegen"} diff --git a/frontend/rust-lib/flowy-database/tests/main.rs b/frontend/rust-lib/flowy-database/tests/main.rs index 3a9960ec68..d9afd095bf 100644 --- a/frontend/rust-lib/flowy-database/tests/main.rs +++ b/frontend/rust-lib/flowy-database/tests/main.rs @@ -1 +1 @@ -mod database; +// mod database; diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml new file mode 100644 index 0000000000..150493fdac --- /dev/null +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "flowy-database2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +collab = { version = "0.1.0" } +collab-database = { version = "0.1.0" } +collab-persistence = { version = "0.1.0" } + +flowy-derive = { path = "../flowy-derive" } +flowy-notification = { path = "../flowy-notification" } +parking_lot = "0.12.1" +protobuf = {version = "2.28.0"} +flowy-error = { path = "../flowy-error", features = ["adaptor_dispatch", "collab"]} +lib-dispatch = { path = "../lib-dispatch" } +tokio = { version = "1.26", features = ["sync"] } +flowy-task= { path = "../flowy-task" } +bytes = { version = "1.4" } +tracing = { version = "0.1", features = ["log"] } +database-model = { path = "../../../shared-lib/database-model" } +serde = { version = "1.0", features = ["derive"] } +serde_json = {version = "1.0"} +serde_repr = "0.1" +lib-infra = { path = "../../../shared-lib/lib-infra" } +chrono = { version = "0.4.22", default-features = false, features = ["clock"] } +rust_decimal = "1.28.1" +rusty-money = {version = "0.4.1", features = ["iso"]} +lazy_static = "1.4.0" +indexmap = {version = "1.9.2", features = ["serde"]} +url = { version = "2"} +fancy-regex = "0.10.0" +futures = "0.3.26" +dashmap = "5" +anyhow = "1.0" +async-stream = "0.3.4" +rayon = "1.6.1" +nanoid = "0.4.0" + +strum = "0.21" +strum_macros = "0.21" + +[dev-dependencies] +flowy-test = { path = "../flowy-test" } + +[build-dependencies] +flowy-codegen = { path = "../flowy-codegen"} + + +[features] +dart = ["flowy-codegen/dart", "flowy-notification/dart"] +ts = ["flowy-codegen/ts", "flowy-notification/ts"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database2/Flowy.toml b/frontend/rust-lib/flowy-database2/Flowy.toml new file mode 100644 index 0000000000..b6a237d1c6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/Flowy.toml @@ -0,0 +1,7 @@ +# Check out the FlowyConfig (located in flowy_toml.rs) for more details. +proto_input = [ + "src/event_map.rs", + "src/entities", + "src/notification.rs" +] +event_files = ["src/event_map.rs"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database2/build.rs b/frontend/rust-lib/flowy-database2/build.rs new file mode 100644 index 0000000000..06388d2a02 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/build.rs @@ -0,0 +1,10 @@ +fn main() { + let crate_name = env!("CARGO_PKG_NAME"); + flowy_codegen::protobuf_file::gen(crate_name); + + #[cfg(feature = "dart")] + flowy_codegen::dart_event::gen(crate_name); + + #[cfg(feature = "ts")] + flowy_codegen::ts_event::gen(crate_name); +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs new file mode 100644 index 0000000000..d65699acb1 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs @@ -0,0 +1,137 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::services::setting::{CalendarLayout, CalendarLayoutSetting}; + +#[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf)] +pub struct CalendarLayoutSettingPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub layout_ty: CalendarLayoutPB, + + #[pb(index = 3)] + pub first_day_of_week: i32, + + #[pb(index = 4)] + pub show_weekends: bool, + + #[pb(index = 5)] + pub show_week_numbers: bool, +} + +impl std::convert::From for CalendarLayoutSetting { + fn from(pb: CalendarLayoutSettingPB) -> Self { + CalendarLayoutSetting { + layout_ty: pb.layout_ty.into(), + first_day_of_week: pb.first_day_of_week, + show_weekends: pb.show_weekends, + show_week_numbers: pb.show_week_numbers, + field_id: pb.field_id, + } + } +} + +impl std::convert::From for CalendarLayoutSettingPB { + fn from(params: CalendarLayoutSetting) -> Self { + CalendarLayoutSettingPB { + field_id: params.field_id, + layout_ty: params.layout_ty.into(), + first_day_of_week: params.first_day_of_week, + show_weekends: params.show_weekends, + show_week_numbers: params.show_week_numbers, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Default, ProtoBuf_Enum)] +#[repr(u8)] +pub enum CalendarLayoutPB { + #[default] + MonthLayout = 0, + WeekLayout = 1, + DayLayout = 2, +} + +impl std::convert::From for CalendarLayout { + fn from(pb: CalendarLayoutPB) -> Self { + match pb { + CalendarLayoutPB::MonthLayout => CalendarLayout::Month, + CalendarLayoutPB::WeekLayout => CalendarLayout::Week, + CalendarLayoutPB::DayLayout => CalendarLayout::Day, + } + } +} +impl std::convert::From for CalendarLayoutPB { + fn from(layout: CalendarLayout) -> Self { + match layout { + CalendarLayout::Month => CalendarLayoutPB::MonthLayout, + CalendarLayout::Week => CalendarLayoutPB::WeekLayout, + CalendarLayout::Day => CalendarLayoutPB::DayLayout, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct CalendarEventRequestPB { + #[pb(index = 1)] + pub view_id: String, + + // Currently, requesting the events within the specified month + // is not supported + #[pb(index = 2)] + pub month: String, +} + +#[derive(Debug, Clone, Default)] +pub struct CalendarEventRequestParams { + pub view_id: String, + pub month: String, +} + +impl TryInto for CalendarEventRequestPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + Ok(CalendarEventRequestParams { + view_id: view_id.0, + month: self.month, + }) + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct CalendarEventPB { + #[pb(index = 1)] + pub row_id: i64, + + #[pb(index = 2)] + pub title_field_id: String, + + #[pb(index = 3)] + pub title: String, + + #[pb(index = 4)] + pub timestamp: i64, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RepeatedCalendarEventPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveCalendarEventPB { + #[pb(index = 1)] + pub row_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub timestamp: i64, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs new file mode 100644 index 0000000000..6314f87dc4 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs @@ -0,0 +1,166 @@ +use collab_database::rows::RowId; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::FieldType; + +#[derive(ProtoBuf, Default)] +pub struct CreateSelectOptionPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub view_id: String, + + #[pb(index = 3)] + pub option_name: String, +} + +pub struct CreateSelectOptionParams { + pub field_id: String, + pub view_id: String, + pub option_name: String, +} + +impl TryInto for CreateSelectOptionPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let option_name = + NotEmptyStr::parse(self.option_name).map_err(|_| ErrorCode::SelectOptionNameIsEmpty)?; + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(CreateSelectOptionParams { + field_id: field_id.0, + option_name: option_name.0, + view_id: view_id.0, + }) + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct CellIdPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub row_id: i64, +} + +/// Represents as the cell identifier. It's used to locate the cell in corresponding +/// view's row with the field id. +pub struct CellIdParams { + pub view_id: String, + pub field_id: String, + pub row_id: RowId, +} + +impl TryInto for CellIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(CellIdParams { + view_id: view_id.0, + field_id: field_id.0, + row_id: RowId::from(self.row_id), + }) + } +} + +/// Represents as the data of the cell. +#[derive(Debug, Default, ProtoBuf)] +pub struct CellPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub row_id: i64, + + /// Encoded the data using the helper struct `CellProtobufBlob`. + /// Check out the `CellProtobufBlob` for more information. + #[pb(index = 3)] + pub data: Vec, + + /// the field_type will be None if the field with field_id is not found + #[pb(index = 4, one_of)] + pub field_type: Option, +} + +impl CellPB { + pub fn new(field_id: &str, row_id: i64, field_type: FieldType, data: Vec) -> Self { + Self { + field_id: field_id.to_owned(), + row_id, + data, + field_type: Some(field_type), + } + } + + pub fn empty(field_id: &str, row_id: i64) -> Self { + Self { + field_id: field_id.to_owned(), + row_id, + data: vec![], + field_type: None, + } + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedCellPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::ops::Deref for RepeatedCellPB { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.items + } +} + +impl std::ops::DerefMut for RepeatedCellPB { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.items + } +} + +impl std::convert::From> for RepeatedCellPB { + fn from(items: Vec) -> Self { + Self { items } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct CellChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub row_id: i64, + + #[pb(index = 3)] + pub field_id: String, + + #[pb(index = 4)] + pub cell_changeset: String, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct CellChangesetNotifyPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub row_id: i64, + + #[pb(index = 3)] + pub field_id: String, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs new file mode 100644 index 0000000000..7fe8f08088 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -0,0 +1,226 @@ +use collab_database::rows::RowId; +use collab_database::user::DatabaseRecord; +use collab_database::views::DatabaseLayout; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowPB}; + +/// [DatabasePB] describes how many fields and blocks the grid has +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DatabasePB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub fields: Vec, + + #[pb(index = 3)] + pub rows: Vec, +} + +#[derive(ProtoBuf, Default)] +pub struct CreateDatabasePayloadPB { + #[pb(index = 1)] + pub name: String, +} + +#[derive(Clone, ProtoBuf, Default, Debug)] +pub struct DatabaseViewIdPB { + #[pb(index = 1)] + pub value: String, +} + +impl AsRef for DatabaseViewIdPB { + fn as_ref(&self) -> &str { + &self.value + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveFieldPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub from_index: i32, + + #[pb(index = 4)] + pub to_index: i32, +} + +#[derive(Clone)] +pub struct MoveFieldParams { + pub view_id: String, + pub field_id: String, + pub from_index: i32, + pub to_index: i32, +} + +impl TryInto for MoveFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?; + Ok(MoveFieldParams { + view_id: view_id.0, + field_id: item_id.0, + from_index: self.from_index, + to_index: self.to_index, + }) + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveRowPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_row_id: i64, + + #[pb(index = 3)] + pub to_row_id: i64, +} + +pub struct MoveRowParams { + pub view_id: String, + pub from_row_id: RowId, + pub to_row_id: RowId, +} + +impl TryInto for MoveRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + + Ok(MoveRowParams { + view_id: view_id.0, + from_row_id: RowId::from(self.from_row_id), + to_row_id: RowId::from(self.to_row_id), + }) + } +} +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct MoveGroupRowPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_row_id: i64, + + #[pb(index = 3)] + pub to_group_id: String, + + #[pb(index = 4, one_of)] + pub to_row_id: Option, +} + +pub struct MoveGroupRowParams { + pub view_id: String, + pub from_row_id: RowId, + pub to_group_id: String, + pub to_row_id: Option, +} + +impl TryInto for MoveGroupRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let to_group_id = + NotEmptyStr::parse(self.to_group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + + Ok(MoveGroupRowParams { + view_id: view_id.0, + to_group_id: to_group_id.0, + from_row_id: RowId::from(self.from_row_id), + to_row_id: self.to_row_id.map(RowId::from), + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct DatabaseDescriptionPB { + #[pb(index = 1)] + pub name: String, + + #[pb(index = 2)] + pub database_id: String, +} + +impl From for DatabaseDescriptionPB { + fn from(data: DatabaseRecord) -> Self { + Self { + name: data.name, + database_id: data.database_id, + } + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedDatabaseDescriptionPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DatabaseGroupIdPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub group_id: String, +} + +pub struct DatabaseGroupIdParams { + pub view_id: String, + pub group_id: String, +} + +impl TryInto for DatabaseGroupIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + Ok(DatabaseGroupIdParams { + view_id: view_id.0, + group_id: group_id.0, + }) + } +} +#[derive(Clone, ProtoBuf, Default, Debug)] +pub struct DatabaseLayoutIdPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub layout: DatabaseLayoutPB, +} + +#[derive(Clone, Debug)] +pub struct DatabaseLayoutId { + pub view_id: String, + pub layout: DatabaseLayout, +} + +impl TryInto for DatabaseLayoutIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; + let layout = self.layout.into(); + Ok(DatabaseLayoutId { + view_id: view_id.0, + layout, + }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs new file mode 100644 index 0000000000..2eeef71f58 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -0,0 +1,674 @@ +#![allow(clippy::upper_case_acronyms)] + +use std::fmt::{Display, Formatter}; +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::views::FieldOrder; +use serde_repr::*; +use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::impl_into_field_type; + +/// [FieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc. +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct FieldPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub name: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub visibility: bool, + + #[pb(index = 5)] + pub width: i32, + + #[pb(index = 6)] + pub is_primary: bool, +} + +impl std::convert::From for FieldPB { + fn from(field: Field) -> Self { + Self { + id: field.id, + name: field.name, + field_type: FieldType::from(field.field_type), + visibility: field.visibility, + width: field.width as i32, + is_primary: field.is_primary, + } + } +} + +/// [FieldIdPB] id of the [Field] +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct FieldIdPB { + #[pb(index = 1)] + pub field_id: String, +} + +impl std::convert::From<&str> for FieldIdPB { + fn from(s: &str) -> Self { + FieldIdPB { + field_id: s.to_owned(), + } + } +} + +impl std::convert::From for FieldIdPB { + fn from(s: String) -> Self { + FieldIdPB { field_id: s } + } +} + +impl From for FieldIdPB { + fn from(field_order: FieldOrder) -> Self { + Self { + field_id: field_order.id, + } + } +} + +impl std::convert::From<&Arc> for FieldIdPB { + fn from(field_rev: &Arc) -> Self { + Self { + field_id: field_rev.id.clone(), + } + } +} +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DatabaseFieldChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub inserted_fields: Vec, + + #[pb(index = 3)] + pub deleted_fields: Vec, + + #[pb(index = 4)] + pub updated_fields: Vec, +} + +impl DatabaseFieldChangesetPB { + pub fn insert(database_id: &str, inserted_fields: Vec) -> Self { + Self { + view_id: database_id.to_owned(), + inserted_fields, + deleted_fields: vec![], + updated_fields: vec![], + } + } + + pub fn delete(database_id: &str, deleted_fields: Vec) -> Self { + Self { + view_id: database_id.to_string(), + inserted_fields: vec![], + deleted_fields, + updated_fields: vec![], + } + } + + pub fn update(database_id: &str, updated_fields: Vec) -> Self { + Self { + view_id: database_id.to_string(), + inserted_fields: vec![], + deleted_fields: vec![], + updated_fields, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct IndexFieldPB { + #[pb(index = 1)] + pub field: FieldPB, + + #[pb(index = 2)] + pub index: i32, +} + +impl IndexFieldPB { + pub fn from_field(field: Field, index: usize) -> Self { + Self { + field: FieldPB::from(field), + index: index as i32, + } + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct CreateFieldPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_type: FieldType, + + #[pb(index = 3, one_of)] + pub type_option_data: Option>, +} + +#[derive(Clone)] +pub struct CreateFieldParams { + pub view_id: String, + pub field_type: FieldType, + pub type_option_data: Option>, +} + +impl TryInto for CreateFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + Ok(CreateFieldParams { + view_id: view_id.0, + field_type: self.field_type, + type_option_data: self.type_option_data, + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct UpdateFieldTypePayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub create_if_not_exist: bool, +} + +pub struct EditFieldParams { + pub view_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl TryInto for UpdateFieldTypePayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(EditFieldParams { + view_id: view_id.0, + field_id: field_id.0, + field_type: self.field_type, + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct TypeOptionPathPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, +} + +pub struct TypeOptionPathParams { + pub view_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl TryInto for TypeOptionPathPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let database_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(TypeOptionPathParams { + view_id: database_id.0, + field_id: field_id.0, + field_type: self.field_type, + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct TypeOptionPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field: FieldPB, + + #[pb(index = 3)] + pub type_option_data: Vec, +} + +/// Collection of the [FieldPB] +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedFieldPB { + #[pb(index = 1)] + pub items: Vec, +} +impl std::ops::Deref for RepeatedFieldPB { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.items + } +} + +impl std::ops::DerefMut for RepeatedFieldPB { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.items + } +} + +impl std::convert::From> for RepeatedFieldPB { + fn from(items: Vec) -> Self { + Self { items } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RepeatedFieldIdPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::ops::Deref for RepeatedFieldIdPB { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.items + } +} + +impl std::convert::From> for RepeatedFieldIdPB { + fn from(items: Vec) -> Self { + RepeatedFieldIdPB { items } + } +} + +impl std::convert::From for RepeatedFieldIdPB { + fn from(s: String) -> Self { + RepeatedFieldIdPB { + items: vec![FieldIdPB::from(s)], + } + } +} + +/// [TypeOptionChangesetPB] is used to update the type-option data. +#[derive(ProtoBuf, Default)] +pub struct TypeOptionChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + /// Check out [TypeOptionPB] for more details. + #[pb(index = 3)] + pub type_option_data: Vec, +} + +#[derive(Clone)] +pub struct TypeOptionChangesetParams { + pub view_id: String, + pub field_id: String, + pub type_option_data: Vec, +} + +impl TryInto for TypeOptionChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let _ = NotEmptyStr::parse(self.field_id.clone()).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + + Ok(TypeOptionChangesetParams { + view_id: view_id.0, + field_id: self.field_id, + type_option_data: self.type_option_data, + }) + } +} + +#[derive(ProtoBuf, Default)] +pub struct GetFieldPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2, one_of)] + pub field_ids: Option, +} + +pub struct GetFieldParams { + pub view_id: String, + pub field_ids: Option>, +} + +impl TryInto for GetFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_ids = self.field_ids.map(|repeated| { + repeated + .items + .into_iter() + .map(|item| item.field_id) + .collect::>() + }); + + Ok(GetFieldParams { + view_id: view_id.0, + field_ids, + }) + } +} + +/// [FieldChangesetPB] is used to modify the corresponding field. It defines which properties of +/// the field can be modified. +/// +/// Pass in None if you don't want to modify a property +/// Pass in Some(Value) if you want to modify a property +/// +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct FieldChangesetPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub view_id: String, + + #[pb(index = 3, one_of)] + pub name: Option, + + #[pb(index = 4, one_of)] + pub desc: Option, + + #[pb(index = 5, one_of)] + pub field_type: Option, + + #[pb(index = 6, one_of)] + pub frozen: Option, + + #[pb(index = 7, one_of)] + pub visibility: Option, + + #[pb(index = 8, one_of)] + pub width: Option, + // #[pb(index = 9, one_of)] + // pub type_option_data: Option>, +} + +impl TryInto for FieldChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + let field_type = self.field_type.map(FieldType::from); + // if let Some(type_option_data) = self.type_option_data.as_ref() { + // if type_option_data.is_empty() { + // return Err(ErrorCode::TypeOptionDataIsEmpty); + // } + // } + + Ok(FieldChangesetParams { + field_id: field_id.0, + view_id: view_id.0, + name: self.name, + desc: self.desc, + field_type, + frozen: self.frozen, + visibility: self.visibility, + width: self.width, + // type_option_data: self.type_option_data, + }) + } +} + +#[derive(Debug, Clone, Default)] +pub struct FieldChangesetParams { + pub field_id: String, + + pub view_id: String, + + pub name: Option, + + pub desc: Option, + + pub field_type: Option, + + pub frozen: Option, + + pub visibility: Option, + + pub width: Option, + // pub type_option_data: Option>, +} +/// Certain field types have user-defined options such as color, date format, number format, +/// or a list of values for a multi-select list. These options are defined within a specialization +/// of the FieldTypeOption class. +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype) +/// for more information. +/// +/// The order of the enum can't be changed. If you want to add a new type, +/// it would be better to append it to the end of the list. +#[derive( + Debug, + Clone, + PartialEq, + Hash, + Eq, + ProtoBuf_Enum, + EnumCountMacro, + EnumIter, + Serialize_repr, + Deserialize_repr, +)] +#[repr(u8)] +pub enum FieldType { + RichText = 0, + Number = 1, + DateTime = 2, + SingleSelect = 3, + MultiSelect = 4, + Checkbox = 5, + URL = 6, + Checklist = 7, +} + +pub const RICH_TEXT_FIELD: FieldType = FieldType::RichText; +pub const NUMBER_FIELD: FieldType = FieldType::Number; +pub const DATE_FIELD: FieldType = FieldType::DateTime; +pub const SINGLE_SELECT_FIELD: FieldType = FieldType::SingleSelect; +pub const MULTI_SELECT_FIELD: FieldType = FieldType::MultiSelect; +pub const CHECKBOX_FIELD: FieldType = FieldType::Checkbox; +pub const URL_FIELD: FieldType = FieldType::URL; +pub const CHECKLIST_FIELD: FieldType = FieldType::Checklist; + +impl std::default::Default for FieldType { + fn default() -> Self { + FieldType::RichText + } +} + +impl Display for FieldType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let value: i64 = self.clone().into(); + f.write_fmt(format_args!("{}", value)) + } +} + +impl AsRef for FieldType { + fn as_ref(&self) -> &FieldType { + self + } +} + +impl From<&FieldType> for FieldType { + fn from(field_type: &FieldType) -> Self { + field_type.clone() + } +} + +impl FieldType { + pub fn default_cell_width(&self) -> i32 { + match self { + FieldType::DateTime => 180, + _ => 150, + } + } + + pub fn default_name(&self) -> String { + let s = match self { + FieldType::RichText => "Text", + FieldType::Number => "Number", + FieldType::DateTime => "Date", + FieldType::SingleSelect => "Single Select", + FieldType::MultiSelect => "Multi Select", + FieldType::Checkbox => "Checkbox", + FieldType::URL => "URL", + FieldType::Checklist => "Checklist", + }; + s.to_string() + } + + pub fn is_number(&self) -> bool { + self == &NUMBER_FIELD + } + + pub fn is_text(&self) -> bool { + self == &RICH_TEXT_FIELD + } + + pub fn is_checkbox(&self) -> bool { + self == &CHECKBOX_FIELD + } + + pub fn is_date(&self) -> bool { + self == &DATE_FIELD + } + + pub fn is_single_select(&self) -> bool { + self == &SINGLE_SELECT_FIELD + } + + pub fn is_multi_select(&self) -> bool { + self == &MULTI_SELECT_FIELD + } + + pub fn is_url(&self) -> bool { + self == &URL_FIELD + } + + pub fn is_select_option(&self) -> bool { + self == &MULTI_SELECT_FIELD || self == &SINGLE_SELECT_FIELD + } + + pub fn is_check_list(&self) -> bool { + self == &CHECKLIST_FIELD + } + + pub fn can_be_group(&self) -> bool { + self.is_select_option() || self.is_checkbox() || self.is_url() + } +} + +impl_into_field_type!(i64); +impl_into_field_type!(u8); + +impl From for i64 { + fn from(ty: FieldType) -> Self { + match ty { + FieldType::RichText => 0, + FieldType::Number => 1, + FieldType::DateTime => 2, + FieldType::SingleSelect => 3, + FieldType::MultiSelect => 4, + FieldType::Checkbox => 5, + FieldType::URL => 6, + FieldType::Checklist => 7, + } + } +} + +impl From<&FieldType> for i64 { + fn from(ty: &FieldType) -> Self { + ty.clone() as i64 + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DuplicateFieldPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub view_id: String, +} + +// #[derive(Debug, Clone, Default, ProtoBuf)] +// pub struct GridFieldIdentifierPayloadPB { +// #[pb(index = 1)] +// pub field_id: String, +// +// #[pb(index = 2)] +// pub view_id: String, +// } + +impl TryInto for DuplicateFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(FieldIdParams { + view_id: view_id.0, + field_id: field_id.0, + }) + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DeleteFieldPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub view_id: String, +} + +impl TryInto for DeleteFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(FieldIdParams { + view_id: view_id.0, + field_id: field_id.0, + }) + } +} + +pub struct FieldIdParams { + pub field_id: String, + pub view_id: String, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs new file mode 100644 index 0000000000..74b35696fe --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs @@ -0,0 +1,61 @@ +use crate::services::filter::{Filter, FromFilterString}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct CheckboxFilterPB { + #[pb(index = 1)] + pub condition: CheckboxFilterConditionPB, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum CheckboxFilterConditionPB { + IsChecked = 0, + IsUnChecked = 1, +} + +impl std::convert::From for u32 { + fn from(value: CheckboxFilterConditionPB) -> Self { + value as u32 + } +} + +impl std::default::Default for CheckboxFilterConditionPB { + fn default() -> Self { + CheckboxFilterConditionPB::IsChecked + } +} + +impl std::convert::TryFrom for CheckboxFilterConditionPB { + type Error = ErrorCode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(CheckboxFilterConditionPB::IsChecked), + 1 => Ok(CheckboxFilterConditionPB::IsUnChecked), + _ => Err(ErrorCode::InvalidData), + } + } +} + +impl FromFilterString for CheckboxFilterPB { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized, + { + CheckboxFilterPB { + condition: CheckboxFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(CheckboxFilterConditionPB::IsChecked), + } + } +} + +impl std::convert::From<&Filter> for CheckboxFilterPB { + fn from(filter: &Filter) -> Self { + CheckboxFilterPB { + condition: CheckboxFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(CheckboxFilterConditionPB::IsChecked), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs new file mode 100644 index 0000000000..7cc553fd81 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs @@ -0,0 +1,61 @@ +use crate::services::filter::{Filter, FromFilterString}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct ChecklistFilterPB { + #[pb(index = 1)] + pub condition: ChecklistFilterConditionPB, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum ChecklistFilterConditionPB { + IsComplete = 0, + IsIncomplete = 1, +} + +impl std::convert::From for u32 { + fn from(value: ChecklistFilterConditionPB) -> Self { + value as u32 + } +} + +impl std::default::Default for ChecklistFilterConditionPB { + fn default() -> Self { + ChecklistFilterConditionPB::IsIncomplete + } +} + +impl std::convert::TryFrom for ChecklistFilterConditionPB { + type Error = ErrorCode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(ChecklistFilterConditionPB::IsComplete), + 1 => Ok(ChecklistFilterConditionPB::IsIncomplete), + _ => Err(ErrorCode::InvalidData), + } + } +} + +impl FromFilterString for ChecklistFilterPB { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized, + { + ChecklistFilterPB { + condition: ChecklistFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(ChecklistFilterConditionPB::IsIncomplete), + } + } +} + +impl std::convert::From<&Filter> for ChecklistFilterPB { + fn from(filter: &Filter) -> Self { + ChecklistFilterPB { + condition: ChecklistFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(ChecklistFilterConditionPB::IsIncomplete), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs new file mode 100644 index 0000000000..36981cb280 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs @@ -0,0 +1,121 @@ +use crate::services::filter::{Filter, FromFilterString}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct DateFilterPB { + #[pb(index = 1)] + pub condition: DateFilterConditionPB, + + #[pb(index = 2, one_of)] + pub start: Option, + + #[pb(index = 3, one_of)] + pub end: Option, + + #[pb(index = 4, one_of)] + pub timestamp: Option, +} + +#[derive(Deserialize, Serialize, Default, Clone, Debug)] +pub struct DateFilterContentPB { + pub start: Option, + pub end: Option, + pub timestamp: Option, +} + +impl ToString for DateFilterContentPB { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl FromStr for DateFilterContentPB { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum DateFilterConditionPB { + DateIs = 0, + DateBefore = 1, + DateAfter = 2, + DateOnOrBefore = 3, + DateOnOrAfter = 4, + DateWithIn = 5, + DateIsEmpty = 6, + DateIsNotEmpty = 7, +} + +impl std::convert::From for u32 { + fn from(value: DateFilterConditionPB) -> Self { + value as u32 + } +} +impl std::default::Default for DateFilterConditionPB { + fn default() -> Self { + DateFilterConditionPB::DateIs + } +} + +impl std::convert::TryFrom for DateFilterConditionPB { + type Error = ErrorCode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(DateFilterConditionPB::DateIs), + 1 => Ok(DateFilterConditionPB::DateBefore), + 2 => Ok(DateFilterConditionPB::DateAfter), + 3 => Ok(DateFilterConditionPB::DateOnOrBefore), + 4 => Ok(DateFilterConditionPB::DateOnOrAfter), + 5 => Ok(DateFilterConditionPB::DateWithIn), + 6 => Ok(DateFilterConditionPB::DateIsEmpty), + _ => Err(ErrorCode::InvalidData), + } + } +} +impl FromFilterString for DateFilterPB { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized, + { + let condition = DateFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(DateFilterConditionPB::DateIs); + let mut date_filter = DateFilterPB { + condition, + ..Default::default() + }; + + if let Ok(content) = DateFilterContentPB::from_str(&filter.content) { + date_filter.start = content.start; + date_filter.end = content.end; + date_filter.timestamp = content.timestamp; + }; + + date_filter + } +} +impl std::convert::From<&Filter> for DateFilterPB { + fn from(filter: &Filter) -> Self { + let condition = DateFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(DateFilterConditionPB::DateIs); + let mut date_filter = DateFilterPB { + condition, + ..Default::default() + }; + + if let Ok(content) = DateFilterContentPB::from_str(&filter.content) { + date_filter.start = content.start; + date_filter.end = content.end; + date_filter.timestamp = content.timestamp; + }; + + date_filter + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs new file mode 100644 index 0000000000..05a0fbd4ea --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs @@ -0,0 +1,54 @@ +use crate::entities::FilterPB; +use flowy_derive::ProtoBuf; + +#[derive(Debug, Default, ProtoBuf)] +pub struct FilterChangesetNotificationPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub insert_filters: Vec, + + #[pb(index = 3)] + pub delete_filters: Vec, + + #[pb(index = 4)] + pub update_filters: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct UpdatedFilter { + #[pb(index = 1)] + pub filter_id: String, + + #[pb(index = 2, one_of)] + pub filter: Option, +} + +impl FilterChangesetNotificationPB { + pub fn from_insert(view_id: &str, filters: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_filters: filters, + delete_filters: Default::default(), + update_filters: Default::default(), + } + } + pub fn from_delete(view_id: &str, filters: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_filters: Default::default(), + delete_filters: filters, + update_filters: Default::default(), + } + } + + pub fn from_update(view_id: &str, filters: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_filters: Default::default(), + delete_filters: Default::default(), + update_filters: filters, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs new file mode 100644 index 0000000000..d628a13801 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs @@ -0,0 +1,17 @@ +mod checkbox_filter; +mod checklist_filter; +mod date_filter; +mod filter_changeset; +mod number_filter; +mod select_option_filter; +mod text_filter; +mod util; + +pub use checkbox_filter::*; +pub use checklist_filter::*; +pub use date_filter::*; +pub use filter_changeset::*; +pub use number_filter::*; +pub use select_option_filter::*; +pub use text_filter::*; +pub use util::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs new file mode 100644 index 0000000000..0633282ebd --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs @@ -0,0 +1,76 @@ +use crate::services::filter::{Filter, FromFilterString}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct NumberFilterPB { + #[pb(index = 1)] + pub condition: NumberFilterConditionPB, + + #[pb(index = 2)] + pub content: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum NumberFilterConditionPB { + Equal = 0, + NotEqual = 1, + GreaterThan = 2, + LessThan = 3, + GreaterThanOrEqualTo = 4, + LessThanOrEqualTo = 5, + NumberIsEmpty = 6, + NumberIsNotEmpty = 7, +} + +impl std::default::Default for NumberFilterConditionPB { + fn default() -> Self { + NumberFilterConditionPB::Equal + } +} + +impl std::convert::From for u32 { + fn from(value: NumberFilterConditionPB) -> Self { + value as u32 + } +} +impl std::convert::TryFrom for NumberFilterConditionPB { + type Error = ErrorCode; + + fn try_from(n: u8) -> Result { + match n { + 0 => Ok(NumberFilterConditionPB::Equal), + 1 => Ok(NumberFilterConditionPB::NotEqual), + 2 => Ok(NumberFilterConditionPB::GreaterThan), + 3 => Ok(NumberFilterConditionPB::LessThan), + 4 => Ok(NumberFilterConditionPB::GreaterThanOrEqualTo), + 5 => Ok(NumberFilterConditionPB::LessThanOrEqualTo), + 6 => Ok(NumberFilterConditionPB::NumberIsEmpty), + 7 => Ok(NumberFilterConditionPB::NumberIsNotEmpty), + _ => Err(ErrorCode::InvalidData), + } + } +} + +impl FromFilterString for NumberFilterPB { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized, + { + NumberFilterPB { + condition: NumberFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(NumberFilterConditionPB::Equal), + content: filter.content.clone(), + } + } +} +impl std::convert::From<&Filter> for NumberFilterPB { + fn from(filter: &Filter) -> Self { + NumberFilterPB { + condition: NumberFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(NumberFilterConditionPB::Equal), + content: filter.content.clone(), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs new file mode 100644 index 0000000000..82b2d21e39 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs @@ -0,0 +1,72 @@ +use crate::services::field::SelectOptionIds; +use crate::services::filter::{Filter, FromFilterString}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct SelectOptionFilterPB { + #[pb(index = 1)] + pub condition: SelectOptionConditionPB, + + #[pb(index = 2)] + pub option_ids: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum SelectOptionConditionPB { + OptionIs = 0, + OptionIsNot = 1, + OptionIsEmpty = 2, + OptionIsNotEmpty = 3, +} + +impl std::convert::From for u32 { + fn from(value: SelectOptionConditionPB) -> Self { + value as u32 + } +} + +impl std::default::Default for SelectOptionConditionPB { + fn default() -> Self { + SelectOptionConditionPB::OptionIs + } +} + +impl std::convert::TryFrom for SelectOptionConditionPB { + type Error = ErrorCode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(SelectOptionConditionPB::OptionIs), + 1 => Ok(SelectOptionConditionPB::OptionIsNot), + 2 => Ok(SelectOptionConditionPB::OptionIsEmpty), + 3 => Ok(SelectOptionConditionPB::OptionIsNotEmpty), + _ => Err(ErrorCode::InvalidData), + } + } +} +impl FromFilterString for SelectOptionFilterPB { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized, + { + let ids = SelectOptionIds::from(filter.content.clone()); + SelectOptionFilterPB { + condition: SelectOptionConditionPB::try_from(filter.condition as u8) + .unwrap_or(SelectOptionConditionPB::OptionIs), + option_ids: ids.into_inner(), + } + } +} + +impl std::convert::From<&Filter> for SelectOptionFilterPB { + fn from(filter: &Filter) -> Self { + let ids = SelectOptionIds::from(filter.content.clone()); + SelectOptionFilterPB { + condition: SelectOptionConditionPB::try_from(filter.condition as u8) + .unwrap_or(SelectOptionConditionPB::OptionIs), + option_ids: ids.into_inner(), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs new file mode 100644 index 0000000000..d169586df7 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs @@ -0,0 +1,78 @@ +use crate::services::filter::{Filter, FromFilterString}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct TextFilterPB { + #[pb(index = 1)] + pub condition: TextFilterConditionPB, + + #[pb(index = 2)] + pub content: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum TextFilterConditionPB { + Is = 0, + IsNot = 1, + Contains = 2, + DoesNotContain = 3, + StartsWith = 4, + EndsWith = 5, + TextIsEmpty = 6, + TextIsNotEmpty = 7, +} + +impl std::convert::From for u32 { + fn from(value: TextFilterConditionPB) -> Self { + value as u32 + } +} + +impl std::default::Default for TextFilterConditionPB { + fn default() -> Self { + TextFilterConditionPB::Is + } +} + +impl std::convert::TryFrom for TextFilterConditionPB { + type Error = ErrorCode; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(TextFilterConditionPB::Is), + 1 => Ok(TextFilterConditionPB::IsNot), + 2 => Ok(TextFilterConditionPB::Contains), + 3 => Ok(TextFilterConditionPB::DoesNotContain), + 4 => Ok(TextFilterConditionPB::StartsWith), + 5 => Ok(TextFilterConditionPB::EndsWith), + 6 => Ok(TextFilterConditionPB::TextIsEmpty), + 7 => Ok(TextFilterConditionPB::TextIsNotEmpty), + _ => Err(ErrorCode::InvalidData), + } + } +} + +impl FromFilterString for TextFilterPB { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized, + { + TextFilterPB { + condition: TextFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(TextFilterConditionPB::Is), + content: filter.content.clone(), + } + } +} + +impl std::convert::From<&Filter> for TextFilterPB { + fn from(filter: &Filter) -> Self { + TextFilterPB { + condition: TextFilterConditionPB::try_from(filter.condition as u8) + .unwrap_or(TextFilterConditionPB::Is), + content: filter.content.clone(), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs new file mode 100644 index 0000000000..989736867c --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -0,0 +1,235 @@ +use crate::entities::parser::NotEmptyStr; +use crate::entities::{ + CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType, + NumberFilterPB, SelectOptionFilterPB, TextFilterPB, +}; +use crate::services::field::SelectOptionIds; +use crate::services::filter::{Filter, FilterType}; +use bytes::Bytes; +use collab_database::fields::Field; +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; +use std::convert::TryInto; +use std::sync::Arc; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct FilterPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub data: Vec, +} + +impl std::convert::From<&Filter> for FilterPB { + fn from(filter: &Filter) -> Self { + let bytes: Bytes = match filter.field_type { + FieldType::RichText => TextFilterPB::from(filter).try_into().unwrap(), + FieldType::Number => NumberFilterPB::from(filter).try_into().unwrap(), + FieldType::DateTime => DateFilterPB::from(filter).try_into().unwrap(), + FieldType::SingleSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(), + FieldType::MultiSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(), + FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(), + FieldType::Checkbox => CheckboxFilterPB::from(filter).try_into().unwrap(), + FieldType::URL => TextFilterPB::from(filter).try_into().unwrap(), + }; + Self { + id: filter.id.clone(), + field_id: filter.field_id.clone(), + field_type: filter.field_type.clone(), + data: bytes.to_vec(), + } + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct RepeatedFilterPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::convert::From>> for RepeatedFilterPB { + fn from(filters: Vec>) -> Self { + RepeatedFilterPB { + items: filters.into_iter().map(|rev| rev.as_ref().into()).collect(), + } + } +} + +impl std::convert::From> for RepeatedFilterPB { + fn from(items: Vec) -> Self { + Self { items } + } +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct DeleteFilterPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub field_type: FieldType, + + #[pb(index = 3)] + pub filter_id: String, + + #[pb(index = 4)] + pub view_id: String, +} + +impl TryInto for DeleteFilterPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)? + .0; + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + + let filter_id = NotEmptyStr::parse(self.filter_id) + .map_err(|_| ErrorCode::UnexpectedEmptyString)? + .0; + + let filter_type = FilterType { + filter_id: filter_id.clone(), + field_id, + field_type: self.field_type, + }; + + Ok(DeleteFilterParams { + view_id, + filter_id, + filter_type, + }) + } +} + +#[derive(Debug)] +pub struct DeleteFilterParams { + pub view_id: String, + pub filter_id: String, + pub filter_type: FilterType, +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct AlterFilterPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub field_type: FieldType, + + /// Create a new filter if the filter_id is None + #[pb(index = 3, one_of)] + pub filter_id: Option, + + #[pb(index = 4)] + pub data: Vec, + + #[pb(index = 5)] + pub view_id: String, +} + +impl AlterFilterPayloadPB { + #[allow(dead_code)] + pub fn new>( + view_id: &str, + field: &Field, + data: T, + ) -> Self { + let data = data.try_into().unwrap_or_else(|_| Bytes::new()); + let field_type = FieldType::from(field.field_type); + Self { + view_id: view_id.to_owned(), + field_id: field.id.clone(), + field_type, + filter_id: None, + data: data.to_vec(), + } + } +} + +impl TryInto for AlterFilterPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)? + .0; + + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + let filter_id = match self.filter_id { + None => None, + Some(filter_id) => Some( + NotEmptyStr::parse(filter_id) + .map_err(|_| ErrorCode::FilterIdIsEmpty)? + .0, + ), + }; + let condition; + let mut content = "".to_string(); + let bytes: &[u8] = self.data.as_ref(); + + match self.field_type { + FieldType::RichText | FieldType::URL => { + let filter = TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; + condition = filter.condition as u8; + content = filter.content; + }, + FieldType::Checkbox => { + let filter = CheckboxFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; + condition = filter.condition as u8; + }, + FieldType::Number => { + let filter = NumberFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; + condition = filter.condition as u8; + content = filter.content; + }, + FieldType::DateTime => { + let filter = DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; + condition = filter.condition as u8; + content = DateFilterContentPB { + start: filter.start, + end: filter.end, + timestamp: filter.timestamp, + } + .to_string(); + }, + FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => { + let filter = SelectOptionFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; + condition = filter.condition as u8; + content = SelectOptionIds::from(filter.option_ids).to_string(); + }, + } + + Ok(AlterFilterParams { + view_id, + field_id, + filter_id, + field_type: self.field_type, + condition: condition as i64, + content, + }) + } +} + +#[derive(Debug)] +pub struct AlterFilterParams { + pub view_id: String, + pub field_id: String, + /// Create a new filter if the filter_id is None + pub filter_id: Option, + pub field_type: FieldType, + pub condition: i64, + pub content: String, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs new file mode 100644 index 0000000000..3f9e92b212 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs @@ -0,0 +1,75 @@ +use crate::services::group::Group; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct UrlGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct TextGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct SelectOptionGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct GroupRecordPB { + #[pb(index = 1)] + group_id: String, + + #[pb(index = 2)] + visible: bool, +} + +impl std::convert::From for GroupRecordPB { + fn from(rev: Group) -> Self { + Self { + group_id: rev.id, + visible: rev.visible, + } + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct NumberGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct DateGroupConfigurationPB { + #[pb(index = 1)] + pub condition: DateCondition, + + #[pb(index = 2)] + hide_empty: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum DateCondition { + Relative = 0, + Day = 1, + Week = 2, + Month = 3, + Year = 4, +} + +impl std::default::Default for DateCondition { + fn default() -> Self { + DateCondition::Relative + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct CheckboxGroupConfigurationPB { + #[pb(index = 1)] + pub(crate) hide_empty: bool, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs new file mode 100644 index 0000000000..3cd9216254 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -0,0 +1,185 @@ +use std::convert::TryInto; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::{FieldType, RowPB}; +use crate::services::group::{GroupData, GroupSetting}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct GroupSettingPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub field_id: String, +} + +impl std::convert::From<&GroupSetting> for GroupSettingPB { + fn from(rev: &GroupSetting) -> Self { + GroupSettingPB { + id: rev.id.clone(), + field_id: rev.field_id.clone(), + } + } +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct RepeatedGroupPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::ops::Deref for RepeatedGroupPB { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.items + } +} + +impl std::ops::DerefMut for RepeatedGroupPB { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.items + } +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct GroupPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub group_id: String, + + #[pb(index = 3)] + pub desc: String, + + #[pb(index = 4)] + pub rows: Vec, + + #[pb(index = 5)] + pub is_default: bool, + + #[pb(index = 6)] + pub is_visible: bool, +} + +impl std::convert::From for GroupPB { + fn from(group_data: GroupData) -> Self { + Self { + field_id: group_data.field_id, + group_id: group_data.id, + desc: group_data.name, + rows: group_data.rows.into_iter().map(RowPB::from).collect(), + is_default: group_data.is_default, + is_visible: group_data.is_visible, + } + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct RepeatedGroupSettingPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::convert::From> for RepeatedGroupSettingPB { + fn from(items: Vec) -> Self { + Self { items } + } +} + +impl std::convert::From> for RepeatedGroupSettingPB { + fn from(group_settings: Vec) -> Self { + RepeatedGroupSettingPB { + items: group_settings + .iter() + .map(|setting| setting.into()) + .collect(), + } + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct InsertGroupPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub field_type: FieldType, + + #[pb(index = 3)] + pub view_id: String, +} + +impl TryInto for InsertGroupPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::ViewIdIsInvalid)? + .0; + + Ok(InsertGroupParams { + field_id, + field_type: self.field_type, + view_id, + }) + } +} + +pub struct InsertGroupParams { + pub view_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct DeleteGroupPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub group_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub view_id: String, +} + +impl TryInto for DeleteGroupPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + let group_id = NotEmptyStr::parse(self.group_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::ViewIdIsInvalid)? + .0; + + Ok(DeleteGroupParams { + field_id, + field_type: self.field_type, + group_id, + view_id, + }) + } +} + +pub struct DeleteGroupParams { + pub view_id: String, + pub field_id: String, + pub group_id: String, + pub field_type: FieldType, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs new file mode 100644 index 0000000000..b5a2f19d21 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs @@ -0,0 +1,165 @@ +use std::fmt::Formatter; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::{GroupPB, InsertedRowPB, RowPB}; + +#[derive(Debug, Default, ProtoBuf)] +pub struct GroupRowsNotificationPB { + #[pb(index = 1)] + pub group_id: String, + + #[pb(index = 2, one_of)] + pub group_name: Option, + + #[pb(index = 3)] + pub inserted_rows: Vec, + + #[pb(index = 4)] + pub deleted_rows: Vec, + + #[pb(index = 5)] + pub updated_rows: Vec, +} + +impl std::fmt::Display for GroupRowsNotificationPB { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for inserted_row in &self.inserted_rows { + f.write_fmt(format_args!( + "Insert: {} row at {:?}", + inserted_row.row.id, inserted_row.index + ))?; + } + + for deleted_row in &self.deleted_rows { + f.write_fmt(format_args!("Delete: {} row", deleted_row))?; + } + + Ok(()) + } +} + +impl GroupRowsNotificationPB { + pub fn is_empty(&self) -> bool { + self.group_name.is_none() + && self.inserted_rows.is_empty() + && self.deleted_rows.is_empty() + && self.updated_rows.is_empty() + } + + pub fn new(group_id: String) -> Self { + Self { + group_id, + ..Default::default() + } + } + + pub fn name(group_id: String, name: &str) -> Self { + Self { + group_id, + group_name: Some(name.to_owned()), + ..Default::default() + } + } + + pub fn insert(group_id: String, inserted_rows: Vec) -> Self { + Self { + group_id, + inserted_rows, + ..Default::default() + } + } + + pub fn delete(group_id: String, deleted_rows: Vec) -> Self { + Self { + group_id, + deleted_rows, + ..Default::default() + } + } + + pub fn update(group_id: String, updated_rows: Vec) -> Self { + Self { + group_id, + updated_rows, + ..Default::default() + } + } +} +#[derive(Debug, Default, ProtoBuf)] +pub struct MoveGroupPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub from_group_id: String, + + #[pb(index = 3)] + pub to_group_id: String, +} + +#[derive(Debug)] +pub struct MoveGroupParams { + pub view_id: String, + pub from_group_id: String, + pub to_group_id: String, +} + +impl TryInto for MoveGroupPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)? + .0; + let from_group_id = NotEmptyStr::parse(self.from_group_id) + .map_err(|_| ErrorCode::GroupIdIsEmpty)? + .0; + let to_group_id = NotEmptyStr::parse(self.to_group_id) + .map_err(|_| ErrorCode::GroupIdIsEmpty)? + .0; + Ok(MoveGroupParams { + view_id, + from_group_id, + to_group_id, + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct GroupChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub inserted_groups: Vec, + + #[pb(index = 3)] + pub initial_groups: Vec, + + #[pb(index = 4)] + pub deleted_groups: Vec, + + #[pb(index = 5)] + pub update_groups: Vec, +} + +impl GroupChangesetPB { + pub fn is_empty(&self) -> bool { + self.initial_groups.is_empty() + && self.inserted_groups.is_empty() + && self.deleted_groups.is_empty() + && self.update_groups.is_empty() + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct InsertedGroupPB { + #[pb(index = 1)] + pub group: GroupPB, + + #[pb(index = 2)] + pub index: i32, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/mod.rs new file mode 100644 index 0000000000..778eff4cc9 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/mod.rs @@ -0,0 +1,7 @@ +mod configuration; +mod group; +mod group_changeset; + +pub use configuration::*; +pub use group::*; +pub use group_changeset::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs new file mode 100644 index 0000000000..a527d49a00 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -0,0 +1,23 @@ +#[macro_export] +macro_rules! impl_into_field_type { + ($target: ident) => { + impl std::convert::From<$target> for FieldType { + fn from(ty: $target) -> Self { + match ty { + 0 => FieldType::RichText, + 1 => FieldType::Number, + 2 => FieldType::DateTime, + 3 => FieldType::SingleSelect, + 4 => FieldType::MultiSelect, + 5 => FieldType::Checkbox, + 6 => FieldType::URL, + 7 => FieldType::Checklist, + _ => { + tracing::error!("Can't parser FieldType from value: {}", ty); + FieldType::RichText + }, + } + } + } + }; +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/mod.rs new file mode 100644 index 0000000000..387466e7aa --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/mod.rs @@ -0,0 +1,27 @@ +mod calendar_entities; +mod cell_entities; +mod database_entities; +mod field_entities; +pub mod filter_entities; +mod group_entities; +pub mod parser; +mod row_entities; +pub mod setting_entities; +mod sort_entities; +mod view_entities; + +#[macro_use] +mod macros; +mod type_option_entities; + +pub use calendar_entities::*; +pub use cell_entities::*; +pub use database_entities::*; +pub use field_entities::*; +pub use filter_entities::*; +pub use group_entities::*; +pub use row_entities::*; +pub use setting_entities::*; +pub use sort_entities::*; +pub use type_option_entities::*; +pub use view_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/parser.rs b/frontend/rust-lib/flowy-database2/src/entities/parser.rs new file mode 100644 index 0000000000..edad3ee6b8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/parser.rs @@ -0,0 +1,17 @@ +#[derive(Debug)] +pub struct NotEmptyStr(pub String); + +impl NotEmptyStr { + pub fn parse(s: String) -> Result { + if s.trim().is_empty() { + return Err("Input string is empty".to_owned()); + } + Ok(Self(s)) + } +} + +impl AsRef for NotEmptyStr { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs new file mode 100644 index 0000000000..235707d8e6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -0,0 +1,219 @@ +use std::collections::HashMap; + +use collab_database::rows::{Row, RowId}; +use collab_database::views::RowOrder; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::services::database::{InsertedRow, UpdatedRow}; + +/// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. +#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)] +pub struct RowPB { + #[pb(index = 1)] + pub id: i64, + + #[pb(index = 2)] + pub height: i32, +} + +impl std::convert::From<&Row> for RowPB { + fn from(row: &Row) -> Self { + Self { + id: row.id.into(), + height: row.height, + } + } +} + +impl std::convert::From for RowPB { + fn from(row: Row) -> Self { + Self { + id: row.id.into(), + height: row.height, + } + } +} +impl From for RowPB { + fn from(data: RowOrder) -> Self { + Self { + id: data.id.into(), + height: data.height, + } + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct OptionalRowPB { + #[pb(index = 1, one_of)] + pub row: Option, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedRowPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::convert::From> for RepeatedRowPB { + fn from(items: Vec) -> Self { + Self { items } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct InsertedRowPB { + #[pb(index = 1)] + pub row: RowPB, + + #[pb(index = 2, one_of)] + pub index: Option, + + #[pb(index = 3)] + pub is_new: bool, +} + +impl InsertedRowPB { + pub fn new(row: RowPB) -> Self { + Self { + row, + index: None, + is_new: false, + } + } + + pub fn with_index(row: RowPB, index: i32) -> Self { + Self { + row, + index: Some(index), + is_new: false, + } + } +} + +impl std::convert::From for InsertedRowPB { + fn from(row: RowPB) -> Self { + Self { + row, + index: None, + is_new: false, + } + } +} + +impl std::convert::From<&Row> for InsertedRowPB { + fn from(row: &Row) -> Self { + Self::from(RowPB::from(row)) + } +} + +impl From for InsertedRowPB { + fn from(data: InsertedRow) -> Self { + Self { + row: data.row.into(), + index: data.index, + is_new: data.is_new, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct UpdatedRowPB { + #[pb(index = 1)] + pub row: RowPB, + + // represents as the cells that were updated in this row. + #[pb(index = 2)] + pub field_ids: Vec, +} + +impl From for UpdatedRowPB { + fn from(data: UpdatedRow) -> Self { + Self { + row: data.row.into(), + field_ids: data.field_ids, + } + } +} + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct RowIdPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub row_id: i64, +} + +pub struct RowIdParams { + pub view_id: String, + pub row_id: RowId, +} + +impl TryInto for RowIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + + Ok(RowIdParams { + view_id: view_id.0, + row_id: RowId::from(self.row_id), + }) + } +} + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct BlockRowIdPB { + #[pb(index = 1)] + pub block_id: String, + + #[pb(index = 2)] + pub row_id: String, +} + +#[derive(ProtoBuf, Default)] +pub struct CreateRowPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2, one_of)] + pub start_row_id: Option, + + #[pb(index = 3, one_of)] + pub group_id: Option, + + #[pb(index = 4, one_of)] + pub data: Option, +} + +#[derive(ProtoBuf, Default)] +pub struct RowDataPB { + #[pb(index = 1)] + pub cell_data_by_field_id: HashMap, +} + +#[derive(Default)] +pub struct CreateRowParams { + pub view_id: String, + pub start_row_id: Option, + pub group_id: Option, + pub cell_data_by_field_id: Option>, +} + +impl TryInto for CreateRowPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + let start_row_id = self.start_row_id.map(RowId::from); + Ok(CreateRowParams { + view_id: view_id.0, + start_row_id, + group_id: self.group_id, + cell_data_by_field_id: self.data.map(|data| data.cell_data_by_field_id), + }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs new file mode 100644 index 0000000000..b5430fa18b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -0,0 +1,213 @@ +use std::convert::TryInto; + +use collab_database::views::DatabaseLayout; +use strum_macros::EnumIter; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::{ + AlterFilterParams, AlterFilterPayloadPB, AlterSortParams, AlterSortPayloadPB, + CalendarLayoutSettingPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, + DeleteGroupPayloadPB, DeleteSortParams, DeleteSortPayloadPB, InsertGroupParams, + InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB, +}; +use crate::services::setting::CalendarLayoutSetting; + +/// [DatabaseViewSettingPB] defines the setting options for the grid. Such as the filter, group, and sort. +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct DatabaseViewSettingPB { + #[pb(index = 1)] + pub current_layout: DatabaseLayoutPB, + + #[pb(index = 2)] + pub layout_setting: LayoutSettingPB, + + #[pb(index = 3)] + pub filters: RepeatedFilterPB, + + #[pb(index = 4)] + pub group_settings: RepeatedGroupSettingPB, + + #[pb(index = 5)] + pub sorts: RepeatedSortPB, +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)] +#[repr(u8)] +pub enum DatabaseLayoutPB { + Grid = 0, + Board = 1, + Calendar = 2, +} + +impl std::default::Default for DatabaseLayoutPB { + fn default() -> Self { + DatabaseLayoutPB::Grid + } +} + +impl std::convert::From for DatabaseLayoutPB { + fn from(rev: DatabaseLayout) -> Self { + match rev { + DatabaseLayout::Grid => DatabaseLayoutPB::Grid, + DatabaseLayout::Board => DatabaseLayoutPB::Board, + DatabaseLayout::Calendar => DatabaseLayoutPB::Calendar, + } + } +} + +impl std::convert::From for DatabaseLayout { + fn from(layout: DatabaseLayoutPB) -> Self { + match layout { + DatabaseLayoutPB::Grid => DatabaseLayout::Grid, + DatabaseLayoutPB::Board => DatabaseLayout::Board, + DatabaseLayoutPB::Calendar => DatabaseLayout::Calendar, + } + } +} + +#[derive(Default, ProtoBuf)] +pub struct DatabaseSettingChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub layout_type: DatabaseLayoutPB, + + #[pb(index = 3, one_of)] + pub alter_filter: Option, + + #[pb(index = 4, one_of)] + pub delete_filter: Option, + + #[pb(index = 5, one_of)] + pub insert_group: Option, + + #[pb(index = 6, one_of)] + pub delete_group: Option, + + #[pb(index = 7, one_of)] + pub alter_sort: Option, + + #[pb(index = 8, one_of)] + pub delete_sort: Option, +} + +impl TryInto for DatabaseSettingChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::ViewIdIsInvalid)? + .0; + + let insert_filter = match self.alter_filter { + None => None, + Some(payload) => Some(payload.try_into()?), + }; + + let delete_filter = match self.delete_filter { + None => None, + Some(payload) => Some(payload.try_into()?), + }; + + let insert_group = match self.insert_group { + Some(payload) => Some(payload.try_into()?), + None => None, + }; + + let delete_group = match self.delete_group { + Some(payload) => Some(payload.try_into()?), + None => None, + }; + + let alert_sort = match self.alter_sort { + None => None, + Some(payload) => Some(payload.try_into()?), + }; + + let delete_sort = match self.delete_sort { + None => None, + Some(payload) => Some(payload.try_into()?), + }; + + Ok(DatabaseSettingChangesetParams { + view_id, + layout_type: self.layout_type.into(), + insert_filter, + delete_filter, + insert_group, + delete_group, + alert_sort, + delete_sort, + }) + } +} + +pub struct DatabaseSettingChangesetParams { + pub view_id: String, + pub layout_type: DatabaseLayout, + pub insert_filter: Option, + pub delete_filter: Option, + pub insert_group: Option, + pub delete_group: Option, + pub alert_sort: Option, + pub delete_sort: Option, +} + +impl DatabaseSettingChangesetParams { + pub fn is_filter_changed(&self) -> bool { + self.insert_filter.is_some() || self.delete_filter.is_some() + } +} + +#[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)] +pub struct LayoutSettingPB { + #[pb(index = 1, one_of)] + pub calendar: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct LayoutSettingParams { + pub calendar: Option, +} + +impl From for LayoutSettingPB { + fn from(data: LayoutSettingParams) -> Self { + Self { + calendar: data.calendar.map(|calendar| calendar.into()), + } + } +} + +#[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)] +pub struct LayoutSettingChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2, one_of)] + pub calendar: Option, +} + +#[derive(Debug)] +pub struct LayoutSettingChangeset { + pub view_id: String, + pub calendar: Option, +} + +impl TryInto for LayoutSettingChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::ViewIdIsInvalid)? + .0; + + Ok(LayoutSettingChangeset { + view_id, + calendar: self.calendar.map(|calendar| calendar.into()), + }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs new file mode 100644 index 0000000000..76b0c53a62 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs @@ -0,0 +1,257 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::FieldType; +use crate::services::sort::{Sort, SortCondition, SortType}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct SortPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub condition: SortConditionPB, +} + +impl std::convert::From<&Sort> for SortPB { + fn from(sort: &Sort) -> Self { + Self { + id: sort.id.clone(), + field_id: sort.field_id.clone(), + field_type: sort.field_type.clone(), + condition: sort.condition.into(), + } + } +} + +impl std::convert::From for SortPB { + fn from(sort: Sort) -> Self { + Self { + id: sort.id, + field_id: sort.field_id, + field_type: sort.field_type, + condition: sort.condition.into(), + } + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct RepeatedSortPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::convert::From> for RepeatedSortPB { + fn from(revs: Vec) -> Self { + RepeatedSortPB { + items: revs.into_iter().map(|sort| sort.into()).collect(), + } + } +} + +impl std::convert::From> for RepeatedSortPB { + fn from(items: Vec) -> Self { + Self { items } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[repr(u8)] +pub enum SortConditionPB { + Ascending = 0, + Descending = 1, +} +impl std::default::Default for SortConditionPB { + fn default() -> Self { + Self::Ascending + } +} + +impl std::convert::From for SortConditionPB { + fn from(condition: SortCondition) -> Self { + match condition { + SortCondition::Ascending => SortConditionPB::Ascending, + SortCondition::Descending => SortConditionPB::Descending, + } + } +} +impl std::convert::From for SortCondition { + fn from(condition: SortConditionPB) -> Self { + match condition { + SortConditionPB::Ascending => SortCondition::Ascending, + SortConditionPB::Descending => SortCondition::Descending, + } + } +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct AlterSortPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + /// Create a new sort if the sort_id is None + #[pb(index = 4, one_of)] + pub sort_id: Option, + + #[pb(index = 5)] + pub condition: SortConditionPB, +} + +impl TryInto for AlterSortPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)? + .0; + + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + + let sort_id = match self.sort_id { + None => None, + Some(sort_id) => Some( + NotEmptyStr::parse(sort_id) + .map_err(|_| ErrorCode::SortIdIsEmpty)? + .0, + ), + }; + + Ok(AlterSortParams { + view_id, + field_id, + sort_id, + field_type: self.field_type, + condition: self.condition.into(), + }) + } +} + +#[derive(Debug)] +pub struct AlterSortParams { + pub view_id: String, + pub field_id: String, + /// Create a new sort if the sort is None + pub sort_id: Option, + pub field_type: FieldType, + pub condition: SortCondition, +} + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct DeleteSortPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub sort_id: String, +} + +impl TryInto for DeleteSortPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id) + .map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)? + .0; + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; + + let sort_id = NotEmptyStr::parse(self.sort_id) + .map_err(|_| ErrorCode::UnexpectedEmptyString)? + .0; + + let sort_type = SortType { + sort_id: sort_id.clone(), + field_id, + field_type: self.field_type, + }; + + Ok(DeleteSortParams { + view_id, + sort_type, + sort_id, + }) + } +} + +#[derive(Debug, Clone)] +pub struct DeleteSortParams { + pub view_id: String, + pub sort_type: SortType, + pub sort_id: String, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct SortChangesetNotificationPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub insert_sorts: Vec, + + #[pb(index = 3)] + pub delete_sorts: Vec, + + #[pb(index = 4)] + pub update_sorts: Vec, +} + +impl SortChangesetNotificationPB { + pub fn new(view_id: String) -> Self { + Self { + view_id, + insert_sorts: vec![], + delete_sorts: vec![], + update_sorts: vec![], + } + } + + pub fn extend(&mut self, other: SortChangesetNotificationPB) { + self.insert_sorts.extend(other.insert_sorts); + self.delete_sorts.extend(other.delete_sorts); + self.update_sorts.extend(other.update_sorts); + } + + pub fn is_empty(&self) -> bool { + self.insert_sorts.is_empty() && self.delete_sorts.is_empty() && self.update_sorts.is_empty() + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct ReorderAllRowsPB { + #[pb(index = 1)] + pub row_orders: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct ReorderSingleRowPB { + #[pb(index = 1)] + pub row_id: i64, + + #[pb(index = 2)] + pub old_index: i32, + + #[pb(index = 3)] + pub new_index: i32, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs new file mode 100644 index 0000000000..3c4ad10a21 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs @@ -0,0 +1,24 @@ +use crate::services::field::CheckboxTypeOption; +use flowy_derive::ProtoBuf; + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct CheckboxTypeOptionPB { + #[pb(index = 1)] + pub is_selected: bool, +} + +impl From for CheckboxTypeOptionPB { + fn from(data: CheckboxTypeOption) -> Self { + Self { + is_selected: data.is_selected, + } + } +} + +impl From for CheckboxTypeOption { + fn from(data: CheckboxTypeOptionPB) -> Self { + Self { + is_selected: data.is_selected, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs new file mode 100644 index 0000000000..911769c102 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs @@ -0,0 +1,142 @@ +#![allow(clippy::upper_case_acronyms)] + +use strum_macros::EnumIter; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +use crate::entities::CellIdPB; +use crate::services::field::{DateFormat, DateTypeOption, TimeFormat}; + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateCellDataPB { + #[pb(index = 1)] + pub date: String, + + #[pb(index = 2)] + pub time: String, + + #[pb(index = 3)] + pub timestamp: i64, + + #[pb(index = 4)] + pub include_time: bool, +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateChangesetPB { + #[pb(index = 1)] + pub cell_path: CellIdPB, + + #[pb(index = 2, one_of)] + pub date: Option, + + #[pb(index = 3, one_of)] + pub time: Option, + + #[pb(index = 4, one_of)] + pub include_time: Option, + + #[pb(index = 5)] + pub is_utc: bool, +} + +// Date +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateTypeOptionPB { + #[pb(index = 1)] + pub date_format: DateFormatPB, + + #[pb(index = 2)] + pub time_format: TimeFormatPB, + + #[pb(index = 3)] + pub include_time: bool, +} + +impl From for DateTypeOptionPB { + fn from(data: DateTypeOption) -> Self { + Self { + date_format: data.date_format.into(), + time_format: data.time_format.into(), + include_time: data.include_time, + } + } +} + +impl From for DateTypeOption { + fn from(data: DateTypeOptionPB) -> Self { + Self { + date_format: data.date_format.into(), + time_format: data.time_format.into(), + include_time: data.include_time, + } + } +} + +#[derive(Clone, Debug, Copy, EnumIter, ProtoBuf_Enum)] +pub enum DateFormatPB { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, + DayMonthYear = 4, +} +impl std::default::Default for DateFormatPB { + fn default() -> Self { + DateFormatPB::Friendly + } +} + +impl From for DateFormat { + fn from(data: DateFormatPB) -> Self { + match data { + DateFormatPB::Local => DateFormat::Local, + DateFormatPB::US => DateFormat::US, + DateFormatPB::ISO => DateFormat::ISO, + DateFormatPB::Friendly => DateFormat::Friendly, + DateFormatPB::DayMonthYear => DateFormat::DayMonthYear, + } + } +} + +impl From for DateFormatPB { + fn from(data: DateFormat) -> Self { + match data { + DateFormat::Local => DateFormatPB::Local, + DateFormat::US => DateFormatPB::US, + DateFormat::ISO => DateFormatPB::ISO, + DateFormat::Friendly => DateFormatPB::Friendly, + DateFormat::DayMonthYear => DateFormatPB::DayMonthYear, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, ProtoBuf_Enum)] +pub enum TimeFormatPB { + TwelveHour = 0, + TwentyFourHour = 1, +} + +impl std::default::Default for TimeFormatPB { + fn default() -> Self { + TimeFormatPB::TwentyFourHour + } +} + +impl From for TimeFormat { + fn from(data: TimeFormatPB) -> Self { + match data { + TimeFormatPB::TwelveHour => TimeFormat::TwelveHour, + TimeFormatPB::TwentyFourHour => TimeFormat::TwentyFourHour, + } + } +} + +impl From for TimeFormatPB { + fn from(data: TimeFormat) -> Self { + match data { + TimeFormat::TwelveHour => TimeFormatPB::TwelveHour, + TimeFormat::TwentyFourHour => TimeFormatPB::TwentyFourHour, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs new file mode 100644 index 0000000000..d45c06e575 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs @@ -0,0 +1,13 @@ +mod checkbox_entities; +mod date_entities; +mod number_entities; +mod select_option; +mod text_entities; +mod url_entities; + +pub use checkbox_entities::*; +pub use date_entities::*; +pub use number_entities::*; +pub use select_option::*; +pub use text_entities::*; +pub use url_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs new file mode 100644 index 0000000000..7feddbf412 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs @@ -0,0 +1,177 @@ +use crate::services::field::{NumberFormat, NumberTypeOption}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +// Number +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct NumberTypeOptionPB { + #[pb(index = 1)] + pub format: NumberFormatPB, + + #[pb(index = 2)] + pub scale: u32, + + #[pb(index = 3)] + pub symbol: String, + + #[pb(index = 4)] + pub sign_positive: bool, + + #[pb(index = 5)] + pub name: String, +} + +impl From for NumberTypeOptionPB { + fn from(data: NumberTypeOption) -> Self { + Self { + format: data.format.into(), + scale: data.scale, + symbol: data.symbol, + sign_positive: data.sign_positive, + name: data.name, + } + } +} + +impl From for NumberTypeOption { + fn from(data: NumberTypeOptionPB) -> Self { + Self { + format: data.format.into(), + scale: data.scale, + symbol: data.symbol, + sign_positive: data.sign_positive, + name: data.name, + } + } +} + +#[derive(Clone, Copy, Debug, ProtoBuf_Enum)] +pub enum NumberFormatPB { + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +impl std::default::Default for NumberFormatPB { + fn default() -> Self { + NumberFormatPB::Num + } +} + +impl From for NumberFormatPB { + fn from(data: NumberFormat) -> Self { + match data { + NumberFormat::Num => NumberFormatPB::Num, + NumberFormat::USD => NumberFormatPB::USD, + NumberFormat::CanadianDollar => NumberFormatPB::CanadianDollar, + NumberFormat::EUR => NumberFormatPB::EUR, + NumberFormat::Pound => NumberFormatPB::Pound, + NumberFormat::Yen => NumberFormatPB::Yen, + NumberFormat::Ruble => NumberFormatPB::Ruble, + NumberFormat::Rupee => NumberFormatPB::Rupee, + NumberFormat::Won => NumberFormatPB::Won, + NumberFormat::Yuan => NumberFormatPB::Yuan, + NumberFormat::Real => NumberFormatPB::Real, + NumberFormat::Lira => NumberFormatPB::Lira, + NumberFormat::Rupiah => NumberFormatPB::Rupiah, + NumberFormat::Franc => NumberFormatPB::Franc, + NumberFormat::HongKongDollar => NumberFormatPB::HongKongDollar, + NumberFormat::NewZealandDollar => NumberFormatPB::NewZealandDollar, + NumberFormat::Krona => NumberFormatPB::Krona, + NumberFormat::NorwegianKrone => NumberFormatPB::NorwegianKrone, + NumberFormat::MexicanPeso => NumberFormatPB::MexicanPeso, + NumberFormat::Rand => NumberFormatPB::Rand, + NumberFormat::NewTaiwanDollar => NumberFormatPB::NewTaiwanDollar, + NumberFormat::DanishKrone => NumberFormatPB::DanishKrone, + NumberFormat::Baht => NumberFormatPB::Baht, + NumberFormat::Forint => NumberFormatPB::Forint, + NumberFormat::Koruna => NumberFormatPB::Koruna, + NumberFormat::Shekel => NumberFormatPB::Shekel, + NumberFormat::ChileanPeso => NumberFormatPB::ChileanPeso, + NumberFormat::PhilippinePeso => NumberFormatPB::PhilippinePeso, + NumberFormat::Dirham => NumberFormatPB::Dirham, + NumberFormat::ColombianPeso => NumberFormatPB::ColombianPeso, + NumberFormat::Riyal => NumberFormatPB::Riyal, + NumberFormat::Ringgit => NumberFormatPB::Ringgit, + NumberFormat::Leu => NumberFormatPB::Leu, + NumberFormat::ArgentinePeso => NumberFormatPB::ArgentinePeso, + NumberFormat::UruguayanPeso => NumberFormatPB::UruguayanPeso, + NumberFormat::Percent => NumberFormatPB::Percent, + } + } +} + +impl From for NumberFormat { + fn from(data: NumberFormatPB) -> Self { + match data { + NumberFormatPB::Num => NumberFormat::Num, + NumberFormatPB::USD => NumberFormat::USD, + NumberFormatPB::CanadianDollar => NumberFormat::CanadianDollar, + NumberFormatPB::EUR => NumberFormat::EUR, + NumberFormatPB::Pound => NumberFormat::Pound, + NumberFormatPB::Yen => NumberFormat::Yen, + NumberFormatPB::Ruble => NumberFormat::Ruble, + NumberFormatPB::Rupee => NumberFormat::Rupee, + NumberFormatPB::Won => NumberFormat::Won, + NumberFormatPB::Yuan => NumberFormat::Yuan, + NumberFormatPB::Real => NumberFormat::Real, + NumberFormatPB::Lira => NumberFormat::Lira, + NumberFormatPB::Rupiah => NumberFormat::Rupiah, + NumberFormatPB::Franc => NumberFormat::Franc, + NumberFormatPB::HongKongDollar => NumberFormat::HongKongDollar, + NumberFormatPB::NewZealandDollar => NumberFormat::NewZealandDollar, + NumberFormatPB::Krona => NumberFormat::Krona, + NumberFormatPB::NorwegianKrone => NumberFormat::NorwegianKrone, + NumberFormatPB::MexicanPeso => NumberFormat::MexicanPeso, + NumberFormatPB::Rand => NumberFormat::Rand, + NumberFormatPB::NewTaiwanDollar => NumberFormat::NewTaiwanDollar, + NumberFormatPB::DanishKrone => NumberFormat::DanishKrone, + NumberFormatPB::Baht => NumberFormat::Baht, + NumberFormatPB::Forint => NumberFormat::Forint, + NumberFormatPB::Koruna => NumberFormat::Koruna, + NumberFormatPB::Shekel => NumberFormat::Shekel, + NumberFormatPB::ChileanPeso => NumberFormat::ChileanPeso, + NumberFormatPB::PhilippinePeso => NumberFormat::PhilippinePeso, + NumberFormatPB::Dirham => NumberFormat::Dirham, + NumberFormatPB::ColombianPeso => NumberFormat::ColombianPeso, + NumberFormatPB::Riyal => NumberFormat::Riyal, + NumberFormatPB::Ringgit => NumberFormat::Ringgit, + NumberFormatPB::Leu => NumberFormat::Leu, + NumberFormatPB::ArgentinePeso => NumberFormat::ArgentinePeso, + NumberFormatPB::UruguayanPeso => NumberFormat::UruguayanPeso, + NumberFormatPB::Percent => NumberFormat::Percent, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option.rs new file mode 100644 index 0000000000..286eb64375 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option.rs @@ -0,0 +1,320 @@ +use crate::entities::parser::NotEmptyStr; +use crate::entities::{CellIdPB, CellIdParams}; +use crate::services::field::{ + ChecklistTypeOption, MultiSelectTypeOption, SelectOption, SelectOptionColor, + SingleSelectTypeOption, +}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +/// [SelectOptionPB] represents an option for a single select, and multiple select. +#[derive(Clone, Debug, Default, PartialEq, Eq, ProtoBuf)] +pub struct SelectOptionPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub name: String, + + #[pb(index = 3)] + pub color: SelectOptionColorPB, +} + +impl From for SelectOptionPB { + fn from(data: SelectOption) -> Self { + Self { + id: data.id, + name: data.name, + color: data.color.into(), + } + } +} + +impl From for SelectOption { + fn from(data: SelectOptionPB) -> Self { + Self { + id: data.id, + name: data.name, + color: data.color.into(), + } + } +} + +#[derive(Default, ProtoBuf)] +pub struct RepeatedSelectOptionPayload { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub row_id: i64, + + #[pb(index = 4)] + pub items: Vec, +} + +#[derive(ProtoBuf_Enum, PartialEq, Eq, Debug, Clone)] +#[repr(u8)] +pub enum SelectOptionColorPB { + Purple = 0, + Pink = 1, + LightPink = 2, + Orange = 3, + Yellow = 4, + Lime = 5, + Green = 6, + Aqua = 7, + Blue = 8, +} + +impl std::default::Default for SelectOptionColorPB { + fn default() -> Self { + SelectOptionColorPB::Purple + } +} + +impl From for SelectOptionColorPB { + fn from(data: SelectOptionColor) -> Self { + match data { + SelectOptionColor::Purple => SelectOptionColorPB::Purple, + SelectOptionColor::Pink => SelectOptionColorPB::Pink, + SelectOptionColor::LightPink => SelectOptionColorPB::LightPink, + SelectOptionColor::Orange => SelectOptionColorPB::Orange, + SelectOptionColor::Yellow => SelectOptionColorPB::Yellow, + SelectOptionColor::Lime => SelectOptionColorPB::Lime, + SelectOptionColor::Green => SelectOptionColorPB::Green, + SelectOptionColor::Aqua => SelectOptionColorPB::Aqua, + SelectOptionColor::Blue => SelectOptionColorPB::Blue, + } + } +} + +impl From for SelectOptionColor { + fn from(data: SelectOptionColorPB) -> Self { + match data { + SelectOptionColorPB::Purple => SelectOptionColor::Purple, + SelectOptionColorPB::Pink => SelectOptionColor::Pink, + SelectOptionColorPB::LightPink => SelectOptionColor::LightPink, + SelectOptionColorPB::Orange => SelectOptionColor::Orange, + SelectOptionColorPB::Yellow => SelectOptionColor::Yellow, + SelectOptionColorPB::Lime => SelectOptionColor::Lime, + SelectOptionColorPB::Green => SelectOptionColor::Green, + SelectOptionColorPB::Aqua => SelectOptionColor::Aqua, + SelectOptionColorPB::Blue => SelectOptionColor::Blue, + } + } +} + +/// [SelectOptionCellDataPB] contains a list of user's selected options and a list of all the options +/// that the cell can use. +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct SelectOptionCellDataPB { + /// The available options that the cell can use. + #[pb(index = 1)] + pub options: Vec, + + /// The selected options for the cell. + #[pb(index = 2)] + pub select_options: Vec, +} + +/// [SelectOptionChangesetPB] describes the changes of a FieldTypeOptionData. For the moment, +/// it is used by [MultiSelectTypeOptionPB] and [SingleSelectTypeOptionPB]. +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct SelectOptionChangesetPB { + #[pb(index = 1)] + pub cell_identifier: CellIdPB, + + #[pb(index = 2)] + pub insert_options: Vec, + + #[pb(index = 3)] + pub update_options: Vec, + + #[pb(index = 4)] + pub delete_options: Vec, +} + +pub struct SelectOptionChangeset { + pub cell_path: CellIdParams, + pub insert_options: Vec, + pub update_options: Vec, + pub delete_options: Vec, +} + +impl TryInto for SelectOptionChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let cell_identifier = self.cell_identifier.try_into()?; + Ok(SelectOptionChangeset { + cell_path: cell_identifier, + insert_options: self.insert_options, + update_options: self.update_options, + delete_options: self.delete_options, + }) + } +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct SelectOptionCellChangesetPB { + #[pb(index = 1)] + pub cell_identifier: CellIdPB, + + #[pb(index = 2)] + pub insert_option_ids: Vec, + + #[pb(index = 3)] + pub delete_option_ids: Vec, +} + +pub struct SelectOptionCellChangesetParams { + pub cell_identifier: CellIdParams, + pub insert_option_ids: Vec, + pub delete_option_ids: Vec, +} + +impl TryInto for SelectOptionCellChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let cell_identifier: CellIdParams = self.cell_identifier.try_into()?; + let insert_option_ids = self + .insert_option_ids + .into_iter() + .flat_map(|option_id| match NotEmptyStr::parse(option_id) { + Ok(option_id) => Some(option_id.0), + Err(_) => { + tracing::error!("The insert option id should not be empty"); + None + }, + }) + .collect::>(); + + let delete_option_ids = self + .delete_option_ids + .into_iter() + .flat_map(|option_id| match NotEmptyStr::parse(option_id) { + Ok(option_id) => Some(option_id.0), + Err(_) => { + tracing::error!("The deleted option id should not be empty"); + None + }, + }) + .collect::>(); + + Ok(SelectOptionCellChangesetParams { + cell_identifier, + insert_option_ids, + delete_option_ids, + }) + } +} + +// Single select +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct SingleSelectTypeOptionPB { + #[pb(index = 1)] + pub options: Vec, + + #[pb(index = 2)] + pub disable_color: bool, +} + +impl From for SingleSelectTypeOptionPB { + fn from(data: SingleSelectTypeOption) -> Self { + Self { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + disable_color: data.disable_color, + } + } +} + +impl From for SingleSelectTypeOption { + fn from(data: SingleSelectTypeOptionPB) -> Self { + Self { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + disable_color: data.disable_color, + } + } +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct MultiSelectTypeOptionPB { + #[pb(index = 1)] + pub options: Vec, + + #[pb(index = 2)] + pub disable_color: bool, +} + +impl From for MultiSelectTypeOptionPB { + fn from(data: MultiSelectTypeOption) -> Self { + Self { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + disable_color: data.disable_color, + } + } +} + +impl From for MultiSelectTypeOption { + fn from(data: MultiSelectTypeOptionPB) -> Self { + Self { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + disable_color: data.disable_color, + } + } +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct ChecklistTypeOptionPB { + #[pb(index = 1)] + pub options: Vec, + + #[pb(index = 2)] + pub disable_color: bool, +} + +impl From for ChecklistTypeOptionPB { + fn from(data: ChecklistTypeOption) -> Self { + Self { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + disable_color: data.disable_color, + } + } +} + +impl From for ChecklistTypeOption { + fn from(data: ChecklistTypeOptionPB) -> Self { + Self { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + disable_color: data.disable_color, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs new file mode 100644 index 0000000000..cce32dc64a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs @@ -0,0 +1,20 @@ +use crate::services::field::RichTextTypeOption; +use flowy_derive::ProtoBuf; + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RichTextTypeOptionPB { + #[pb(index = 1)] + data: String, +} + +impl From for RichTextTypeOptionPB { + fn from(data: RichTextTypeOption) -> Self { + Self { data: data.inner } + } +} + +impl From for RichTextTypeOption { + fn from(data: RichTextTypeOptionPB) -> Self { + Self { inner: data.data } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs new file mode 100644 index 0000000000..2500145fc8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs @@ -0,0 +1,38 @@ +use crate::services::field::URLTypeOption; +use flowy_derive::ProtoBuf; + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct URLCellDataPB { + #[pb(index = 1)] + pub url: String, + + #[pb(index = 2)] + pub content: String, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct URLTypeOptionPB { + #[pb(index = 1)] + pub url: String, + + #[pb(index = 2)] + pub content: String, +} + +impl From for URLTypeOptionPB { + fn from(data: URLTypeOption) -> Self { + Self { + url: data.url, + content: data.content, + } + } +} + +impl From for URLTypeOption { + fn from(data: URLTypeOptionPB) -> Self { + Self { + url: data.url, + content: data.content, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs new file mode 100644 index 0000000000..70368b7785 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs @@ -0,0 +1,69 @@ +use flowy_derive::ProtoBuf; + +use crate::entities::{InsertedRowPB, UpdatedRowPB}; + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct RowsVisibilityChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 5)] + pub visible_rows: Vec, + + #[pb(index = 6)] + pub invisible_rows: Vec, +} + +#[derive(Debug, Default, Clone, ProtoBuf)] +pub struct RowsChangesetPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub inserted_rows: Vec, + + #[pb(index = 3)] + pub deleted_rows: Vec, + + #[pb(index = 4)] + pub updated_rows: Vec, +} + +impl RowsChangesetPB { + pub fn from_insert(view_id: String, inserted_rows: Vec) -> Self { + Self { + view_id, + inserted_rows, + ..Default::default() + } + } + + pub fn from_delete(view_id: String, deleted_rows: Vec) -> Self { + Self { + view_id, + deleted_rows, + ..Default::default() + } + } + + pub fn from_update(view_id: String, updated_rows: Vec) -> Self { + Self { + view_id, + updated_rows, + ..Default::default() + } + } + + pub fn from_move( + view_id: String, + deleted_rows: Vec, + inserted_rows: Vec, + ) -> Self { + Self { + view_id, + inserted_rows, + deleted_rows, + ..Default::default() + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs new file mode 100644 index 0000000000..e9a70f404a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -0,0 +1,605 @@ +use std::sync::Arc; + +use collab_database::rows::RowId; +use collab_database::views::DatabaseLayout; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; + +use crate::entities::*; +use crate::manager::DatabaseManager2; + +use crate::services::field::{ + type_option_data_from_pb_or_default, DateCellChangeset, SelectOptionCellChangeset, +}; + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_database_data_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_editor = manager.get_database(view_id.as_ref()).await?; + let data = database_editor.get_database_data(view_id.as_ref()).await; + data_result_ok(data) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_database_setting_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_editor = manager.get_database(view_id.as_ref()).await?; + let data = database_editor + .get_database_view_setting(view_id.as_ref()) + .await?; + data_result_ok(data) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn update_database_setting_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: DatabaseSettingChangesetParams = data.into_inner().try_into()?; + let editor = manager.get_database(¶ms.view_id).await?; + + if let Some(insert_params) = params.insert_group { + editor.insert_group(insert_params).await?; + } + + if let Some(delete_params) = params.delete_group { + editor.delete_group(delete_params).await?; + } + + if let Some(alter_filter) = params.insert_filter { + editor.create_or_update_filter(alter_filter).await?; + } + + if let Some(delete_filter) = params.delete_filter { + editor.delete_filter(delete_filter).await?; + } + + if let Some(alter_sort) = params.alert_sort { + let _ = editor.create_or_update_sort(alter_sort).await?; + } + if let Some(delete_sort) = params.delete_sort { + editor.delete_sort(delete_sort).await?; + } + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_all_filters_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_editor = manager.get_database(view_id.as_ref()).await?; + let filters = database_editor.get_all_filters(view_id.as_ref()).await; + data_result_ok(filters) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_all_sorts_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_editor = manager.get_database(view_id.as_ref()).await?; + let sorts = database_editor.get_all_sorts(view_id.as_ref()).await; + data_result_ok(sorts) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn delete_all_sorts_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_editor = manager.get_database(view_id.as_ref()).await?; + database_editor.delete_all_sorts(view_id.as_ref()).await; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_fields_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: GetFieldParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let fields = database_editor + .get_fields(¶ms.view_id, params.field_ids) + .into_iter() + .map(FieldPB::from) + .collect::>() + .into(); + data_result_ok(fields) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn update_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: FieldChangesetParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor.update_field(params).await?; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn update_field_type_option_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: TypeOptionChangesetParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + if let Some(old_field) = database_editor.get_field(¶ms.field_id) { + let field_type = FieldType::from(old_field.field_type); + let type_option_data = + type_option_data_from_pb_or_default(params.type_option_data, &field_type); + database_editor + .update_field_type_option( + ¶ms.view_id, + ¶ms.field_id, + type_option_data, + old_field, + ) + .await?; + } + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn delete_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: FieldIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor.delete_field(¶ms.field_id).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn switch_to_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: EditFieldParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let old_field = database_editor.get_field(¶ms.field_id); + database_editor + .switch_to_field_type(¶ms.field_id, ¶ms.field_type) + .await?; + + if let Some(new_type_option) = database_editor + .get_field(¶ms.field_id) + .map(|field| field.get_any_type_option(field.field_type)) + { + match (old_field, new_type_option) { + (Some(old_field), Some(new_type_option)) => { + database_editor + .update_field_type_option( + ¶ms.view_id, + ¶ms.field_id, + new_type_option, + old_field, + ) + .await?; + }, + _ => { + tracing::warn!("Old field and the new type option should not be empty"); + }, + } + } + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn duplicate_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: FieldIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .duplicate_field(¶ms.view_id, ¶ms.field_id) + .await?; + Ok(()) +} + +/// Return the FieldTypeOptionData if the Field exists otherwise return record not found error. +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_field_type_option_data_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: TypeOptionPathParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + if let Some((field, data)) = database_editor + .get_field_type_option_data(¶ms.field_id) + .await + { + let data = TypeOptionPB { + view_id: params.view_id, + field: FieldPB::from(field), + type_option_data: data.to_vec(), + }; + data_result_ok(data) + } else { + Err(FlowyError::record_not_found()) + } +} + +/// Create FieldMeta and save it. Return the FieldTypeOptionData. +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn create_field_type_option_data_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: CreateFieldParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let (field, data) = database_editor + .create_field_with_type_option(¶ms.view_id, ¶ms.field_type, params.type_option_data) + .await; + + let data = TypeOptionPB { + view_id: params.view_id, + field: FieldPB::from(field), + type_option_data: data.to_vec(), + }; + data_result_ok(data) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn move_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: MoveFieldParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .move_field( + ¶ms.view_id, + ¶ms.field_id, + params.from_index, + params.to_index, + ) + .await?; + Ok(()) +} + +// #[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn get_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: RowIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let row = database_editor.get_row(params.row_id).map(RowPB::from); + data_result_ok(OptionalRowPB { row }) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn delete_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: RowIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor.delete_row(params.row_id).await; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn duplicate_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: RowIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .duplicate_row(¶ms.view_id, params.row_id) + .await; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn move_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: MoveRowParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .move_row(¶ms.view_id, params.from_row_id, params.to_row_id) + .await; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn create_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: CreateRowParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + match database_editor.create_row(params).await? { + None => Err(FlowyError::internal().context("Create row fail")), + Some(row) => data_result_ok(RowPB::from(row)), + } +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn get_cell_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: CellIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let cell = database_editor + .get_cell(¶ms.field_id, params.row_id) + .await; + data_result_ok(cell) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn update_cell_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: CellChangesetPB = data.into_inner(); + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .update_cell_with_changeset( + ¶ms.view_id, + RowId::from(params.row_id), + ¶ms.field_id, + params.cell_changeset.clone(), + ) + .await; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn new_select_option_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: CreateSelectOptionParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let result = database_editor + .create_select_option(¶ms.field_id, params.option_name) + .await; + match result { + None => { + Err(FlowyError::record_not_found().context("Create select option fail. Can't find the field")) + }, + Some(pb) => data_result_ok(pb), + } +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn insert_or_update_select_option_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params = data.into_inner(); + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .insert_select_options( + ¶ms.view_id, + ¶ms.field_id, + RowId::from(params.row_id), + params.items, + ) + .await; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn delete_select_option_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params = data.into_inner(); + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .delete_select_options( + ¶ms.view_id, + ¶ms.field_id, + RowId::from(params.row_id), + params.items, + ) + .await; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn get_select_option_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: CellIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let options = database_editor + .get_select_options(params.row_id, ¶ms.field_id) + .await; + data_result_ok(options) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn update_select_option_cell_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let params: SelectOptionCellChangesetParams = data.into_inner().try_into()?; + let database_editor = manager + .get_database(¶ms.cell_identifier.view_id) + .await?; + let changeset = SelectOptionCellChangeset { + insert_option_ids: params.insert_option_ids, + delete_option_ids: params.delete_option_ids, + }; + database_editor + .update_cell_with_changeset( + ¶ms.cell_identifier.view_id, + params.cell_identifier.row_id, + ¶ms.cell_identifier.field_id, + changeset, + ) + .await; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn update_date_cell_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let data = data.into_inner(); + let cell_id: CellIdParams = data.cell_path.try_into()?; + let cell_changeset = DateCellChangeset { + date: data.date, + time: data.time, + include_time: data.include_time, + is_utc: data.is_utc, + }; + let database_editor = manager.get_database(&cell_id.view_id).await?; + database_editor + .update_cell_with_changeset( + &cell_id.view_id, + cell_id.row_id, + &cell_id.field_id, + cell_changeset, + ) + .await; + Ok(()) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn get_groups_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: DatabaseViewIdPB = data.into_inner(); + let database_editor = manager.get_database(params.as_ref()).await?; + let groups = database_editor.load_groups(params.as_ref()).await?; + data_result_ok(groups) +} + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn get_group_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: DatabaseGroupIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let group = database_editor + .get_group(¶ms.view_id, ¶ms.group_id) + .await?; + data_result_ok(group) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn move_group_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let params: MoveGroupParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .move_group(¶ms.view_id, ¶ms.from_group_id, ¶ms.to_group_id) + .await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn move_group_row_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let params: MoveGroupRowParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + database_editor + .move_group_row( + ¶ms.view_id, + ¶ms.to_group_id, + params.from_row_id, + params.to_row_id, + ) + .await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(manager), err)] +pub(crate) async fn get_databases_handler( + manager: AFPluginState>, +) -> DataResult { + let data = manager.get_all_databases_description().await; + data_result_ok(data) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn set_layout_setting_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let params: LayoutSettingChangeset = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let layout_params = LayoutSettingParams { + calendar: params.calendar, + }; + database_editor + .set_layout_setting(¶ms.view_id, DatabaseLayout::Calendar, layout_params) + .await; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn get_layout_setting_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: DatabaseLayoutId = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let layout_setting_pb = database_editor + .get_layout_setting(¶ms.view_id, params.layout) + .await + .map(LayoutSettingPB::from) + .unwrap_or_default(); + data_result_ok(layout_setting_pb) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn get_calendar_events_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: CalendarEventRequestParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let events = database_editor + .get_all_calendar_events(¶ms.view_id) + .await; + data_result_ok(RepeatedCalendarEventPB { items: events }) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn get_calendar_event_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: RowIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database(¶ms.view_id).await?; + let event = database_editor + .get_calendar_event(¶ms.view_id, params.row_id) + .await; + match event { + None => Err(FlowyError::record_not_found()), + Some(event) => data_result_ok(event), + } +} diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs new file mode 100644 index 0000000000..72203be6f0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -0,0 +1,265 @@ +use std::sync::Arc; + +use strum_macros::Display; + +use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; +use lib_dispatch::prelude::*; + +use crate::event_handler::*; +use crate::manager::DatabaseManager2; + +pub fn init(database_manager: Arc) -> AFPlugin { + let mut plugin = AFPlugin::new() + .name(env!("CARGO_PKG_NAME")) + .state(database_manager); + plugin = plugin + .event(DatabaseEvent::GetDatabase, get_database_data_handler) + .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) + .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) + .event(DatabaseEvent::GetAllFilters, get_all_filters_handler) + .event(DatabaseEvent::GetAllSorts, get_all_sorts_handler) + .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler) + // Field + .event(DatabaseEvent::GetFields, get_fields_handler) + .event(DatabaseEvent::UpdateField, update_field_handler) + .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) + .event(DatabaseEvent::DeleteField, delete_field_handler) + .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) + .event(DatabaseEvent::DuplicateField, duplicate_field_handler) + .event(DatabaseEvent::MoveField, move_field_handler) + .event(DatabaseEvent::GetTypeOption, get_field_type_option_data_handler) + .event(DatabaseEvent::CreateTypeOption, create_field_type_option_data_handler) + // Row + .event(DatabaseEvent::CreateRow, create_row_handler) + .event(DatabaseEvent::GetRow, get_row_handler) + .event(DatabaseEvent::DeleteRow, delete_row_handler) + .event(DatabaseEvent::DuplicateRow, duplicate_row_handler) + .event(DatabaseEvent::MoveRow, move_row_handler) + // Cell + .event(DatabaseEvent::GetCell, get_cell_handler) + .event(DatabaseEvent::UpdateCell, update_cell_handler) + // SelectOption + .event(DatabaseEvent::CreateSelectOption, new_select_option_handler) + .event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler) + .event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler) + .event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler) + .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) + // Date + .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) + // Group + .event(DatabaseEvent::MoveGroup, move_group_handler) + .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) + .event(DatabaseEvent::GetGroups, get_groups_handler) + .event(DatabaseEvent::GetGroup, get_group_handler) + // Database + .event(DatabaseEvent::GetDatabases, get_databases_handler) + // Calendar + .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) + .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler) + // Layout setting + .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler) + .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler); + + plugin +} + +/// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) +/// out, it includes how to use these annotations: input, output, etc. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] +#[event_err = "FlowyError"] +pub enum DatabaseEvent { + /// [GetDatabase] event is used to get the [DatabasePB] + /// + /// The event handler accepts a [DatabaseViewIdPB] and returns a [DatabasePB] if there are no errors. + #[event(input = "DatabaseViewIdPB", output = "DatabasePB")] + GetDatabase = 0, + + /// [GetDatabaseSetting] event is used to get the database's settings. + /// + /// The event handler accepts [DatabaseViewIdPB] and return [DatabaseViewSettingPB] + /// if there is no errors. + #[event(input = "DatabaseViewIdPB", output = "DatabaseViewSettingPB")] + GetDatabaseSetting = 2, + + /// [UpdateDatabaseSetting] event is used to update the database's settings. + /// + /// The event handler accepts [DatabaseSettingChangesetPB] and return errors if failed to modify the grid's settings. + #[event(input = "DatabaseSettingChangesetPB")] + UpdateDatabaseSetting = 3, + + #[event(input = "DatabaseViewIdPB", output = "RepeatedFilterPB")] + GetAllFilters = 4, + + #[event(input = "DatabaseViewIdPB", output = "RepeatedSortPB")] + GetAllSorts = 5, + + #[event(input = "DatabaseViewIdPB")] + DeleteAllSorts = 6, + + /// [GetFields] event is used to get the database's fields. + /// + /// The event handler accepts a [GetFieldPayloadPB] and returns a [RepeatedFieldPB] + /// if there are no errors. + #[event(input = "GetFieldPayloadPB", output = "RepeatedFieldPB")] + GetFields = 10, + + /// [UpdateField] event is used to update a field's attributes. + /// + /// The event handler accepts a [FieldChangesetPB] and returns errors if failed to modify the + /// field. + #[event(input = "FieldChangesetPB")] + UpdateField = 11, + + /// [UpdateFieldTypeOption] event is used to update the field's type-option data. Certain field + /// types have user-defined options such as color, date format, number format, or a list of values + /// for a multi-select list. These options are defined within a specialization of the + /// FieldTypeOption class. + /// + /// Check out [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype) + /// for more information. + /// + /// The event handler accepts a [TypeOptionChangesetPB] and returns errors if failed to modify the + /// field. + #[event(input = "TypeOptionChangesetPB")] + UpdateFieldTypeOption = 12, + + /// [DeleteField] event is used to delete a Field. [DeleteFieldPayloadPB] is the context that + /// is used to delete the field from the Database. + #[event(input = "DeleteFieldPayloadPB")] + DeleteField = 14, + + /// [UpdateFieldType] event is used to update the current Field's type. + /// It will insert a new FieldTypeOptionData if the new FieldType doesn't exist before, otherwise + /// reuse the existing FieldTypeOptionData. You could check the [DatabaseRevisionPad] for more details. + #[event(input = "UpdateFieldTypePayloadPB")] + UpdateFieldType = 20, + + /// [DuplicateField] event is used to duplicate a Field. The duplicated field data is kind of + /// deep copy of the target field. The passed in [DuplicateFieldPayloadPB] is the context that is + /// used to duplicate the field. + /// + /// Return errors if failed to duplicate the field. + /// + #[event(input = "DuplicateFieldPayloadPB")] + DuplicateField = 21, + + /// [MoveItem] event is used to move an item. For the moment, Item has two types defined in + /// [MoveItemTypePB]. + #[event(input = "MoveFieldPayloadPB")] + MoveField = 22, + + /// [TypeOptionPathPB] event is used to get the FieldTypeOption data for a specific field type. + /// + /// Check out the [TypeOptionPB] for more details. If the [FieldTypeOptionData] does exist + /// for the target type, the [TypeOptionBuilder] will create the default data for that type. + /// + /// Return the [TypeOptionPB] if there are no errors. + #[event(input = "TypeOptionPathPB", output = "TypeOptionPB")] + GetTypeOption = 23, + + /// [CreateTypeOption] event is used to create a new FieldTypeOptionData. + #[event(input = "CreateFieldPayloadPB", output = "TypeOptionPB")] + CreateTypeOption = 24, + + /// [CreateSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if + /// there are no errors. + #[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")] + CreateSelectOption = 30, + + /// [GetSelectOptionCellData] event is used to get the select option data for cell editing. + /// [CellIdPB] locate which cell data that will be read from. The return value, [SelectOptionCellDataPB] + /// contains the available options and the currently selected options. + #[event(input = "CellIdPB", output = "SelectOptionCellDataPB")] + GetSelectOptionCellData = 31, + + /// [InsertOrUpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is + /// FieldType::SingleSelect or FieldType::MultiSelect. + /// + /// This event may trigger the DatabaseNotification::DidUpdateCell event. + /// For example, DatabaseNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPB] + /// carries a change that updates the name of the option. + #[event(input = "RepeatedSelectOptionPayload")] + InsertOrUpdateSelectOption = 32, + + #[event(input = "RepeatedSelectOptionPayload")] + DeleteSelectOption = 33, + + #[event(input = "CreateRowPayloadPB", output = "RowPB")] + CreateRow = 50, + + /// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables + /// to return a nullable row data. + #[event(input = "RowIdPB", output = "OptionalRowPB")] + GetRow = 51, + + #[event(input = "RowIdPB")] + DeleteRow = 52, + + #[event(input = "RowIdPB")] + DuplicateRow = 53, + + #[event(input = "MoveRowPayloadPB")] + MoveRow = 54, + + #[event(input = "CellIdPB", output = "CellPB")] + GetCell = 70, + + /// [UpdateCell] event is used to update the cell content. The passed in data, [CellChangesetPB], + /// carries the changes that will be applied to the cell content by calling `update_cell` function. + /// + /// The 'content' property of the [CellChangesetPB] is a String type. It can be used directly if the + /// cell uses string data. For example, the TextCell or NumberCell. + /// + /// But,it can be treated as a generic type, because we can use [serde] to deserialize the string + /// into a specific data type. For the moment, the 'content' will be deserialized to a concrete type + /// when the FieldType is SingleSelect, DateTime, and MultiSelect. Please see + /// the [UpdateSelectOptionCell] and [UpdateDateCell] events for more details. + #[event(input = "CellChangesetPB")] + UpdateCell = 71, + + /// [UpdateSelectOptionCell] event is used to update a select option cell's data. [SelectOptionCellChangesetPB] + /// contains options that will be deleted or inserted. It can be cast to [CellChangesetPB] that + /// will be used by the `update_cell` function. + #[event(input = "SelectOptionCellChangesetPB")] + UpdateSelectOptionCell = 72, + + /// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPB] + /// contains the date and the time string. It can be cast to [CellChangesetPB] that + /// will be used by the `update_cell` function. + #[event(input = "DateChangesetPB")] + UpdateDateCell = 80, + + #[event(input = "DatabaseViewIdPB", output = "RepeatedGroupPB")] + GetGroups = 100, + + #[event(input = "DatabaseGroupIdPB", output = "GroupPB")] + GetGroup = 101, + + #[event(input = "MoveGroupPayloadPB")] + MoveGroup = 111, + + #[event(input = "MoveGroupRowPayloadPB")] + MoveGroupRow = 112, + + #[event(input = "MoveGroupRowPayloadPB")] + GroupByField = 113, + + /// Returns all the databases + #[event(output = "RepeatedDatabaseDescriptionPB")] + GetDatabases = 114, + + #[event(input = "LayoutSettingChangesetPB")] + SetLayoutSetting = 115, + + #[event(input = "DatabaseLayoutIdPB", output = "LayoutSettingPB")] + GetLayoutSetting = 116, + + #[event(input = "CalendarEventRequestPB", output = "RepeatedCalendarEventPB")] + GetAllCalendarEvents = 117, + + #[event(input = "RowIdPB", output = "CalendarEventPB")] + GetCalendarEvent = 118, + + #[event(input = "MoveCalendarEventPB")] + MoveCalendarEvent = 119, +} diff --git a/frontend/rust-lib/flowy-database2/src/lib.rs b/frontend/rust-lib/flowy-database2/src/lib.rs new file mode 100644 index 0000000000..5e9c988c86 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/lib.rs @@ -0,0 +1,10 @@ +pub use manager::*; + +pub mod entities; +mod event_handler; +pub mod event_map; +mod manager; +mod notification; +mod protobuf; +pub mod services; +pub mod template; diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs new file mode 100644 index 0000000000..11967ee766 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -0,0 +1,215 @@ +use collab::plugin_impl::rocks_disk::Config; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; + +use collab_database::database::DatabaseData; +use collab_database::user::UserDatabase as InnerUserDatabase; +use collab_database::views::{CreateDatabaseParams, CreateViewParams}; +use collab_persistence::kv::rocks_kv::RocksCollabDB; +use parking_lot::Mutex; +use tokio::sync::RwLock; + +use flowy_error::{FlowyError, FlowyResult}; +use flowy_task::TaskDispatcher; + +use crate::entities::{DatabaseDescriptionPB, DatabaseLayoutPB, RepeatedDatabaseDescriptionPB}; +use crate::services::database::{DatabaseEditor, MutexDatabase}; + +pub trait DatabaseUser2: Send + Sync { + fn user_id(&self) -> Result; + fn token(&self) -> Result; + fn kv_db(&self) -> Result, FlowyError>; +} + +pub struct DatabaseManager2 { + user: Arc, + user_database: UserDatabase, + task_scheduler: Arc>, + editors: RwLock>>, +} + +impl DatabaseManager2 { + pub fn new( + database_user: Arc, + task_scheduler: Arc>, + ) -> Self { + Self { + user: database_user, + user_database: UserDatabase::default(), + task_scheduler, + editors: Default::default(), + } + } + + pub async fn initialize(&self, user_id: i64, _token: &str) -> FlowyResult<()> { + let db = self.user.kv_db()?; + *self.user_database.lock() = Some(InnerUserDatabase::new( + user_id, + db, + Config::default() + .enable_snapshot(true) + .snapshot_per_update(10), + )); + // do nothing + Ok(()) + } + + pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> { + self.initialize(user_id, token).await?; + Ok(()) + } + + pub async fn get_all_databases_description(&self) -> RepeatedDatabaseDescriptionPB { + let databases_description = self.with_user_database(vec![], |database| { + database + .get_all_databases() + .into_iter() + .map(DatabaseDescriptionPB::from) + .collect() + }); + + RepeatedDatabaseDescriptionPB { + items: databases_description, + } + } + + pub async fn get_database(&self, view_id: &str) -> FlowyResult> { + let database_id = self.with_user_database(Err(FlowyError::internal()), |database| { + database + .get_database_id_with_view_id(view_id) + .ok_or_else(FlowyError::record_not_found) + })?; + + if let Some(editor) = self.editors.read().await.get(&database_id) { + return Ok(editor.clone()); + } + + let mut editors = self.editors.write().await; + let database = MutexDatabase::new(self.with_user_database( + Err(FlowyError::record_not_found()), + |database| { + database + .get_database(&database_id) + .ok_or_else(FlowyError::record_not_found) + }, + )?); + + let editor = Arc::new(DatabaseEditor::new(database, self.task_scheduler.clone()).await?); + editors.insert(database_id.to_string(), editor.clone()); + Ok(editor) + } + + #[tracing::instrument(level = "debug", skip_all)] + pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> { + let view_id = view_id.as_ref(); + let database_id = self.with_user_database(None, |database| { + database.get_database_id_with_view_id(view_id) + }); + + if let Some(database_id) = database_id { + let mut editors = self.editors.write().await; + if let Some(editor) = editors.get(&database_id) { + if editor.close_view_editor(view_id).await { + editor.close().await; + editors.remove(&database_id); + } + } + } + + Ok(()) + } + + pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult> { + let database_data = self.with_user_database(Err(FlowyError::internal()), |database| { + let data = database.get_database_duplicated_data(view_id)?; + let json_bytes = data.to_json_bytes()?; + Ok(json_bytes) + })?; + + Ok(database_data) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn create_database_with_database_data( + &self, + view_id: &str, + data: Vec, + ) -> FlowyResult<()> { + let mut database_data = DatabaseData::from_json_bytes(data)?; + database_data.view.id = view_id.to_string(); + self.with_user_database( + Err(FlowyError::internal().context("Create database with data failed")), + |database| { + let database = database.create_database_with_data(database_data)?; + Ok(database) + }, + )?; + Ok(()) + } + + pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> { + let _ = self.with_user_database( + Err(FlowyError::internal().context("Create database with params failed")), + |user_database| { + let database = user_database.create_database(params)?; + Ok(database) + }, + )?; + Ok(()) + } + + pub async fn create_linked_view( + &self, + name: String, + layout: DatabaseLayoutPB, + database_id: String, + target_view_id: String, + duplicated_view_id: Option, + ) -> FlowyResult<()> { + self.with_user_database( + Err(FlowyError::internal().context("Create database view failed")), + |user_database| { + let database = user_database + .get_database(&database_id) + .ok_or_else(FlowyError::record_not_found)?; + match duplicated_view_id { + None => { + let params = CreateViewParams::new(database_id, target_view_id, name, layout.into()); + database.create_linked_view(params); + }, + Some(duplicated_view_id) => { + database.duplicate_linked_view(&duplicated_view_id); + }, + } + Ok(()) + }, + )?; + Ok(()) + } + + fn with_user_database(&self, default_value: Output, f: F) -> Output + where + F: FnOnce(&InnerUserDatabase) -> Output, + { + let database = self.user_database.lock(); + match &*database { + None => default_value, + Some(folder) => f(folder), + } + } +} + +#[derive(Clone, Default)] +pub struct UserDatabase(Arc>>); + +impl Deref for UserDatabase { + type Target = Arc>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +unsafe impl Sync for UserDatabase {} + +unsafe impl Send for UserDatabase {} diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs new file mode 100644 index 0000000000..0a693d4ebe --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -0,0 +1,55 @@ +use flowy_derive::ProtoBuf_Enum; +use flowy_notification::NotificationBuilder; +const OBSERVABLE_CATEGORY: &str = "Grid"; + +#[derive(ProtoBuf_Enum, Debug)] +pub enum DatabaseNotification { + Unknown = 0, + /// Trigger after inserting/deleting/updating a row + DidUpdateViewRows = 20, + /// Trigger when the visibility of the row was changed. For example, updating the filter will trigger the notification + DidUpdateViewRowsVisibility = 21, + /// Trigger after inserting/deleting/updating a field + DidUpdateFields = 22, + /// Trigger after editing a cell + DidUpdateCell = 40, + /// Trigger after editing a field properties including rename,update type option, etc + DidUpdateField = 50, + /// Trigger after the number of groups is changed + DidUpdateGroups = 60, + /// Trigger after inserting/deleting/updating/moving a row + DidUpdateGroupRow = 61, + /// Trigger when setting a new grouping field + DidGroupByField = 62, + /// Trigger after inserting/deleting/updating a filter + DidUpdateFilter = 63, + /// Trigger after inserting/deleting/updating a sort + DidUpdateSort = 64, + /// Trigger after the sort configurations are changed + DidReorderRows = 65, + /// Trigger after editing the row that hit the sort rule + DidReorderSingleRow = 66, + /// Trigger when the settings of the database are changed + DidUpdateSettings = 70, + // Trigger when the layout setting of the database is updated + DidUpdateLayoutSettings = 80, + // Trigger when the layout field of the database is changed + DidSetNewLayoutField = 81, +} + +impl std::default::Default for DatabaseNotification { + fn default() -> Self { + DatabaseNotification::Unknown + } +} + +impl std::convert::From for i32 { + fn from(notification: DatabaseNotification) -> Self { + notification as i32 + } +} + +#[tracing::instrument(level = "trace")] +pub fn send_notification(id: &str, ty: DatabaseNotification) -> NotificationBuilder { + NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY) +} diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs new file mode 100644 index 0000000000..03bce144da --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs @@ -0,0 +1,126 @@ +use parking_lot::RwLock; +use std::any::{type_name, Any}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::sync::Arc; + +pub type CellCache = Arc>>; +pub type CellFilterCache = Arc>>; + +#[derive(Default, Debug)] +/// The better option is use LRU cache +pub struct AnyTypeCache(HashMap); + +impl AnyTypeCache +where + TypeValueKey: Clone + Hash + Eq, +{ + pub fn new() -> Arc>> { + Arc::new(RwLock::new(AnyTypeCache(HashMap::default()))) + } + + pub fn insert(&mut self, key: &TypeValueKey, val: T) -> Option + where + T: 'static + Send + Sync, + { + self + .0 + .insert(key.clone(), TypeValue::new(val)) + .and_then(downcast_owned) + } + + pub fn remove(&mut self, key: &TypeValueKey) { + self.0.remove(key); + } + + // pub fn remove>(&mut self, key: K) -> Option + // where + // T: 'static + Send + Sync, + // { + // self.0.remove(key.as_ref()).and_then(downcast_owned) + // } + + pub fn get(&self, key: &TypeValueKey) -> Option<&T> + where + T: 'static + Send + Sync, + { + self + .0 + .get(key) + .and_then(|type_value| type_value.boxed.downcast_ref()) + } + + pub fn get_mut(&mut self, key: &TypeValueKey) -> Option<&mut T> + where + T: 'static + Send + Sync, + { + self + .0 + .get_mut(key) + .and_then(|type_value| type_value.boxed.downcast_mut()) + } + + pub fn contains(&self, key: &TypeValueKey) -> bool { + self.0.contains_key(key) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +fn downcast_owned(type_value: TypeValue) -> Option { + type_value.boxed.downcast().ok().map(|boxed| *boxed) +} + +#[derive(Debug)] +struct TypeValue { + boxed: Box, + #[allow(dead_code)] + ty: &'static str, +} + +impl TypeValue { + pub fn new(value: T) -> Self + where + T: Send + Sync + 'static, + { + Self { + boxed: Box::new(value), + ty: type_name::(), + } + } +} + +impl std::ops::Deref for TypeValue { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.boxed + } +} + +impl std::ops::DerefMut for TypeValue { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.boxed + } +} + +// #[cfg(test)] +// mod tests { +// use crate::services::cell::CellDataCache; +// +// #[test] +// fn test() { +// let mut ext = CellDataCache::new(); +// ext.insert("1", "a".to_string()); +// ext.insert("2", 2); +// +// let a: &String = ext.get("1").unwrap(); +// assert_eq!(a, "a"); +// +// let a: Option<&usize> = ext.get("1"); +// assert!(a.is_none()); +// } +// } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs new file mode 100644 index 0000000000..b5b4561bc8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -0,0 +1,442 @@ +use std::collections::HashMap; +use std::fmt::Debug; + +use collab_database::fields::Field; +use collab_database::rows::{get_field_type_from_cell, Cell, Cells}; + +use flowy_error::{ErrorCode, FlowyResult}; + +use crate::entities::FieldType; +use crate::services::cell::{CellCache, CellProtobufBlob}; +use crate::services::field::*; +use crate::services::group::make_no_status_group; + +/// Decode the opaque cell data into readable format content +pub trait CellDataDecoder: TypeOption { + /// + /// Tries to decode the opaque cell string to `decoded_field_type`'s cell data. Sometimes, the `field_type` + /// of the `FieldRevision` is not equal to the `decoded_field_type`(This happened When switching + /// the field type of the `FieldRevision` to another field type). So the cell data is need to do + /// some transformation. + /// + /// For example, the current field type of the `FieldRevision` is a checkbox. When switching the field + /// type from the checkbox to single select, it will create two new options,`Yes` and `No`, if they don't exist. + /// But the data of the cell doesn't change. We can't iterate all the rows to transform the cell + /// data that can be parsed by the current field type. One approach is to transform the cell data + /// when it get read. For the moment, the cell data is a string, `Yes` or `No`. It needs to compare + /// with the option's name, if match return the id of the option. + fn decode_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field: &Field, + ) -> FlowyResult<::CellData>; + + /// Same as `decode_cell_data` does but Decode the cell data to readable `String` + /// For example, The string of the Multi-Select cell will be a list of the option's name + /// separated by a comma. + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String; + + fn decode_cell_to_str(&self, cell: &Cell) -> String; +} + +pub trait CellDataChangeset: TypeOption { + /// The changeset is able to parse into the concrete data struct if `TypeOption::CellChangeset` + /// implements the `FromCellChangesetString` trait. + /// For example,the SelectOptionCellChangeset,DateCellChangeset. etc. + /// + fn apply_changeset( + &self, + changeset: ::CellChangeset, + cell: Option, + ) -> FlowyResult<(Cell, ::CellData)>; +} + +/// changeset: It will be deserialized into specific data base on the FieldType. +/// For example, +/// FieldType::RichText => String +/// FieldType::SingleSelect => SelectOptionChangeset +/// +/// cell_rev: It will be None if the cell does not contain any data. +pub fn apply_cell_data_changeset( + changeset: C, + cell: Option, + field: &Field, + cell_data_cache: Option, +) -> Cell { + let changeset = changeset.to_cell_changeset_str(); + let field_type = FieldType::from(field.field_type); + match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) + .get_type_option_cell_data_handler(&field_type) + { + None => Cell::default(), + Some(handler) => handler + .handle_cell_changeset(changeset, cell, field) + .unwrap_or_default(), + } +} + +pub fn get_type_cell_protobuf( + cell: &Cell, + field: &Field, + cell_cache: Option, +) -> CellProtobufBlob { + let from_field_type = get_field_type_from_cell(cell); + if from_field_type.is_none() { + return CellProtobufBlob::default(); + } + + let from_field_type = from_field_type.unwrap(); + let to_field_type = FieldType::from(field.field_type); + match try_decode_cell_str_to_cell_protobuf( + cell, + &from_field_type, + &to_field_type, + field, + cell_cache, + ) { + Ok(cell_bytes) => cell_bytes, + Err(e) => { + tracing::error!("Decode cell data failed, {:?}", e); + CellProtobufBlob::default() + }, + } +} + +pub fn get_type_cell_data( + cell: &Cell, + field: &Field, + cell_data_cache: Option, +) -> Option +where + Output: Default + 'static, +{ + let from_field_type = get_field_type_from_cell(cell)?; + let to_field_type = FieldType::from(field.field_type); + try_decode_cell_to_cell_data( + cell, + &from_field_type, + &to_field_type, + field, + cell_data_cache, + ) +} + +/// Decode the opaque cell data from one field type to another using the corresponding `TypeOption` +/// +/// The cell data might become an empty string depends on the to_field_type's `TypeOption` +/// support transform the from_field_type's cell data or not. +/// +/// # Arguments +/// +/// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the +/// `FromCellString` trait. +/// * `from_field_type`: the original field type of the passed-in cell data. Check the `TypeCellData` +/// that is used to save the origin field type of the cell data. +/// * `to_field_type`: decode the passed-in cell data to this field type. It will use the to_field_type's +/// TypeOption to decode this cell data. +/// * `field_rev`: used to get the corresponding TypeOption for the specified field type. +/// +/// returns: CellBytes +/// +pub fn try_decode_cell_str_to_cell_protobuf( + cell: &Cell, + from_field_type: &FieldType, + to_field_type: &FieldType, + field: &Field, + cell_data_cache: Option, +) -> FlowyResult { + match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) + .get_type_option_cell_data_handler(to_field_type) + { + None => Ok(CellProtobufBlob::default()), + Some(handler) => handler.handle_cell_str(cell, from_field_type, field), + } +} + +pub fn try_decode_cell_to_cell_data( + cell: &Cell, + from_field_type: &FieldType, + to_field_type: &FieldType, + field: &Field, + cell_data_cache: Option, +) -> Option { + let handler = TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) + .get_type_option_cell_data_handler(to_field_type)?; + handler + .get_cell_data(cell, from_field_type, field) + .ok()? + .unbox_or_none::() +} +/// Returns a string that represents the current field_type's cell data. +/// For example, The string of the Multi-Select cell will be a list of the option's name +/// separated by a comma. +/// +/// # Arguments +/// +/// * `cell_str`: the opaque cell string that can be decoded by corresponding structs that implement the +/// `FromCellString` trait. +/// * `decoded_field_type`: the field_type of the cell_str +/// * `field_type`: use this field type's `TypeOption` to stringify this cell_str +/// * `field_rev`: used to get the corresponding TypeOption for the specified field type. +/// +/// returns: String +pub fn stringify_cell_data( + cell: &Cell, + decoded_field_type: &FieldType, + field_type: &FieldType, + field: &Field, +) -> String { + match TypeOptionCellExt::new_with_cell_data_cache(field, None) + .get_type_option_cell_data_handler(field_type) + { + None => "".to_string(), + Some(handler) => handler.stringify_cell_str(cell, decoded_field_type, field), + } +} + +pub fn insert_text_cell(s: String, field: &Field) -> Cell { + apply_cell_data_changeset(s, None, field, None) +} + +pub fn insert_number_cell(num: i64, field: &Field) -> Cell { + apply_cell_data_changeset(num.to_string(), None, field, None) +} + +pub fn insert_url_cell(url: String, field: &Field) -> Cell { + // checking if url is equal to group id of no status group because everywhere + // except group of rows with empty url the group id is equal to the url + // so then on the case that url is equal to empty url group id we should change + // the url to empty string + let _no_status_group_id = make_no_status_group(field).id; + let url = match url { + a if a == _no_status_group_id => "".to_owned(), + _ => url, + }; + + apply_cell_data_changeset(url, None, field, None) +} + +pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell { + let s = if is_check { + CHECK.to_string() + } else { + UNCHECK.to_string() + }; + apply_cell_data_changeset(s, None, field, None) +} + +pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell { + let cell_data = serde_json::to_string(&DateCellChangeset { + date: Some(timestamp.to_string()), + time: None, + include_time: Some(false), + is_utc: true, + }) + .unwrap(); + apply_cell_data_changeset(cell_data, None, field, None) +} + +pub fn insert_select_option_cell(option_ids: Vec, field: &Field) -> Cell { + let changeset = + SelectOptionCellChangeset::from_insert_options(option_ids).to_cell_changeset_str(); + apply_cell_data_changeset(changeset, None, field, None) +} + +pub fn delete_select_option_cell(option_ids: Vec, field: &Field) -> Cell { + let changeset = + SelectOptionCellChangeset::from_delete_options(option_ids).to_cell_changeset_str(); + apply_cell_data_changeset(changeset, None, field, None) +} + +/// Deserialize the String into cell specific data type. +pub trait FromCellString { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized; +} + +/// If the changeset applying to the cell is not String type, it should impl this trait. +/// Deserialize the string into cell specific changeset. +pub trait FromCellChangeset { + fn from_changeset(changeset: String) -> FlowyResult + where + Self: Sized; +} + +impl FromCellChangeset for String { + fn from_changeset(changeset: String) -> FlowyResult + where + Self: Sized, + { + Ok(changeset) + } +} + +pub trait ToCellChangeset: Debug { + fn to_cell_changeset_str(&self) -> String; +} + +impl ToCellChangeset for String { + fn to_cell_changeset_str(&self) -> String { + self.clone() + } +} + +pub struct AnyCellChangeset(pub Option); + +impl AnyCellChangeset { + pub fn try_into_inner(self) -> FlowyResult { + match self.0 { + None => Err(ErrorCode::InvalidData.into()), + Some(data) => Ok(data), + } + } +} + +impl std::convert::From for AnyCellChangeset +where + T: FromCellChangeset, +{ + fn from(changeset: C) -> Self { + match T::from_changeset(changeset.to_string()) { + Ok(data) => AnyCellChangeset(Some(data)), + Err(e) => { + tracing::error!("Deserialize CellDataChangeset failed: {}", e); + AnyCellChangeset(None) + }, + } + } +} +// impl std::convert::From for AnyCellChangeset { +// fn from(s: String) -> Self { +// AnyCellChangeset(Some(s)) +// } +// } + +pub struct CellBuilder { + cells: Cells, + field_maps: HashMap, +} + +impl CellBuilder { + pub fn with_cells(cell_by_field_id: HashMap, fields: Vec) -> Self { + let field_maps = fields + .into_iter() + .map(|field| (field.id.clone(), field)) + .collect::>(); + + let mut cells = Cells::new(); + for (field_id, cell_str) in cell_by_field_id { + if let Some(field) = field_maps.get(&field_id) { + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::RichText => { + cells.insert(field_id, insert_text_cell(cell_str, field)); + }, + FieldType::Number => { + if let Ok(num) = cell_str.parse::() { + cells.insert(field_id, insert_number_cell(num, field)); + } + }, + FieldType::DateTime => { + if let Ok(timestamp) = cell_str.parse::() { + cells.insert(field_id, insert_date_cell(timestamp, field)); + } + }, + FieldType::SingleSelect | FieldType::MultiSelect => { + if let Ok(ids) = SelectOptionIds::from_cell_str(&cell_str) { + cells.insert(field_id, insert_select_option_cell(ids.into_inner(), field)); + } + }, + FieldType::Checkbox => { + if let Ok(value) = CheckboxCellData::from_cell_str(&cell_str) { + cells.insert(field_id, insert_checkbox_cell(value.into_inner(), field)); + } + }, + FieldType::URL => { + cells.insert(field_id, insert_url_cell(cell_str, field)); + }, + FieldType::Checklist => { + if let Ok(ids) = SelectOptionIds::from_cell_str(&cell_str) { + cells.insert(field_id, insert_select_option_cell(ids.into_inner(), field)); + } + }, + } + } + } + + CellBuilder { cells, field_maps } + } + + pub fn build(self) -> Cells { + self.cells + } + + pub fn insert_text_cell(&mut self, field_id: &str, data: String) { + match self.field_maps.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the text field with id: {}", field_id), + Some(field) => { + self + .cells + .insert(field_id.to_owned(), insert_text_cell(data, field)); + }, + } + } + + pub fn insert_url_cell(&mut self, field_id: &str, data: String) { + match self.field_maps.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the url field with id: {}", field_id), + Some(field) => { + self + .cells + .insert(field_id.to_owned(), insert_url_cell(data, field)); + }, + } + } + + pub fn insert_number_cell(&mut self, field_id: &str, num: i64) { + match self.field_maps.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the number field with id: {}", field_id), + Some(field) => { + self + .cells + .insert(field_id.to_owned(), insert_number_cell(num, field)); + }, + } + } + + pub fn insert_checkbox_cell(&mut self, field_id: &str, is_check: bool) { + match self.field_maps.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the checkbox field with id: {}", field_id), + Some(field) => { + self + .cells + .insert(field_id.to_owned(), insert_checkbox_cell(is_check, field)); + }, + } + } + + pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) { + match self.field_maps.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the date field with id: {}", field_id), + Some(field) => { + self + .cells + .insert(field_id.to_owned(), insert_date_cell(timestamp, field)); + }, + } + } + + pub fn insert_select_option_cell(&mut self, field_id: &str, option_ids: Vec) { + match self.field_maps.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the select option field with id: {}", field_id), + Some(field) => { + self.cells.insert( + field_id.to_owned(), + insert_select_option_cell(option_ids, field), + ); + }, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/mod.rs b/frontend/rust-lib/flowy-database2/src/services/cell/mod.rs new file mode 100644 index 0000000000..fecc08a024 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/cell/mod.rs @@ -0,0 +1,7 @@ +mod cell_data_cache; +mod cell_operation; +mod type_cell_data; + +pub use cell_data_cache::*; +pub use cell_operation::*; +pub use type_cell_data::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs b/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs new file mode 100644 index 0000000000..45a7bede17 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs @@ -0,0 +1,208 @@ +use crate::entities::FieldType; +use bytes::Bytes; +use database_model::CellRevision; +use flowy_error::{internal_error, FlowyError, FlowyResult}; +use serde::{Deserialize, Serialize}; + +/// TypeCellData is a generic CellData, you can parse the type_cell_data according to the field_type. +/// The `data` is encoded by JSON format. You can use `IntoCellData` to decode the opaque data to +/// concrete cell type. +/// TypeCellData -> IntoCellData -> T +/// +/// The `TypeCellData` is the same as the cell data that was saved to disk except it carries the +/// field_type. The field_type indicates the cell data original `FieldType`. The field_type will +/// be changed if the current Field's type switch from one to another. +/// +#[derive(Debug, Serialize, Deserialize)] +pub struct TypeCellData { + #[serde(rename = "data")] + pub cell_str: String, + pub field_type: FieldType, +} + +impl TypeCellData { + pub fn from_field_type(field_type: &FieldType) -> TypeCellData { + Self { + cell_str: "".to_string(), + field_type: field_type.clone(), + } + } + + pub fn from_json_str(s: &str) -> FlowyResult { + let type_cell_data: TypeCellData = serde_json::from_str(s).map_err(|err| { + let msg = format!("Deserialize {} to type cell data failed.{}", s, err); + FlowyError::internal().context(msg) + })?; + Ok(type_cell_data) + } + + pub fn into_inner(self) -> String { + self.cell_str + } +} + +impl std::convert::TryFrom for TypeCellData { + type Error = FlowyError; + + fn try_from(value: String) -> Result { + TypeCellData::from_json_str(&value) + } +} + +impl ToString for TypeCellData { + fn to_string(&self) -> String { + self.cell_str.clone() + } +} + +impl std::convert::TryFrom<&CellRevision> for TypeCellData { + type Error = FlowyError; + + fn try_from(value: &CellRevision) -> Result { + Self::from_json_str(&value.type_cell_data) + } +} + +impl std::convert::TryFrom for TypeCellData { + type Error = FlowyError; + + fn try_from(value: CellRevision) -> Result { + Self::try_from(&value) + } +} + +impl TypeCellData { + pub fn new(cell_str: String, field_type: FieldType) -> Self { + TypeCellData { + cell_str, + field_type, + } + } + + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "".to_owned()) + } + + pub fn is_number(&self) -> bool { + self.field_type == FieldType::Number + } + + pub fn is_text(&self) -> bool { + self.field_type == FieldType::RichText + } + + pub fn is_checkbox(&self) -> bool { + self.field_type == FieldType::Checkbox + } + + pub fn is_date(&self) -> bool { + self.field_type == FieldType::DateTime + } + + pub fn is_single_select(&self) -> bool { + self.field_type == FieldType::SingleSelect + } + + pub fn is_multi_select(&self) -> bool { + self.field_type == FieldType::MultiSelect + } + + pub fn is_checklist(&self) -> bool { + self.field_type == FieldType::Checklist + } + + pub fn is_url(&self) -> bool { + self.field_type == FieldType::URL + } + + pub fn is_select_option(&self) -> bool { + self.field_type == FieldType::MultiSelect || self.field_type == FieldType::SingleSelect + } +} + +/// The data is encoded by protobuf or utf8. You should choose the corresponding decode struct to parse it. +/// +/// For example: +/// +/// * Use DateCellDataPB to parse the data when the FieldType is Date. +/// * Use URLCellDataPB to parse the data when the FieldType is URL. +/// * Use String to parse the data when the FieldType is RichText, Number, or Checkbox. +/// * Check out the implementation of CellDataOperation trait for more information. +#[derive(Default, Debug)] +pub struct CellProtobufBlob(pub Bytes); + +pub trait DecodedCellData { + type Object; + fn is_empty(&self) -> bool; +} + +pub trait CellProtobufBlobParser { + type Object: DecodedCellData; + fn parser(bytes: &Bytes) -> FlowyResult; +} + +pub trait CellStringParser { + type Object; + fn parser_cell_str(&self, s: &str) -> Option; +} + +pub trait CellBytesCustomParser { + type Object; + fn parse(&self, bytes: &Bytes) -> FlowyResult; +} + +impl CellProtobufBlob { + pub fn new>(data: T) -> Self { + let bytes = Bytes::from(data.as_ref().to_vec()); + Self(bytes) + } + + pub fn from>(bytes: T) -> FlowyResult + where + >::Error: std::fmt::Debug, + { + let bytes = bytes.try_into().map_err(internal_error)?; + Ok(Self(bytes)) + } + + pub fn parser

(&self) -> FlowyResult + where + P: CellProtobufBlobParser, + { + P::parser(&self.0) + } + + pub fn custom_parser

(&self, parser: P) -> FlowyResult + where + P: CellBytesCustomParser, + { + parser.parse(&self.0) + } + + // pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult + // where + // >::Error: std::fmt::Debug, + // { + // T::try_from(self.0.as_ref()).map_err(internal_error) + // } +} + +impl ToString for CellProtobufBlob { + fn to_string(&self) -> String { + match String::from_utf8(self.0.to_vec()) { + Ok(s) => s, + Err(e) => { + tracing::error!("DecodedCellData to string failed: {:?}", e); + "".to_string() + }, + } + } +} + +impl std::ops::Deref for CellProtobufBlob { + type Target = Bytes; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs new file mode 100644 index 0000000000..64023da091 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -0,0 +1,1014 @@ +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; + +use bytes::Bytes; +use collab_database::database::{gen_row_id, timestamp, Database as InnerDatabase}; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{Cell, Cells, Row, RowCell, RowId}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, RowOrder}; +use parking_lot::Mutex; +use tokio::sync::{broadcast, RwLock}; + +use flowy_error::{FlowyError, FlowyResult}; +use flowy_task::TaskDispatcher; +use lib_infra::future::{to_fut, Fut}; + +use crate::entities::{ + AlterFilterParams, AlterSortParams, CalendarEventPB, CellChangesetNotifyPB, CellPB, + CreateRowParams, DatabaseFieldChangesetPB, DatabasePB, DatabaseViewSettingPB, DeleteFilterParams, + DeleteGroupParams, DeleteSortParams, FieldChangesetParams, FieldIdPB, FieldPB, FieldType, + GroupPB, IndexFieldPB, InsertGroupParams, LayoutSettingParams, RepeatedFilterPB, RepeatedGroupPB, + RepeatedSortPB, RowPB, SelectOptionCellDataPB, SelectOptionPB, +}; +use crate::notification::{send_notification, DatabaseNotification}; +use crate::services::cell::{ + apply_cell_data_changeset, get_type_cell_protobuf, AnyTypeCache, CellBuilder, CellCache, + ToCellChangeset, +}; +use crate::services::database::util::database_view_setting_pb_from_view; +use crate::services::database::{DatabaseRowEvent, InsertedRow, UpdatedRow}; +use crate::services::database_view::{ + DatabaseViewChanged, DatabaseViewData, DatabaseViews, RowEventSender, +}; +use crate::services::field::{ + default_type_option_data_for_type, default_type_option_data_from_type, + select_type_option_from_field, transform_type_option, type_option_data_from_pb_or_default, + type_option_to_pb, SelectOptionCellChangeset, SelectOptionIds, TypeOptionCellDataHandler, + TypeOptionCellExt, +}; +use crate::services::filter::Filter; +use crate::services::group::{default_group_setting, GroupSetting, RowChangeset}; +use crate::services::sort::Sort; + +#[derive(Clone)] +pub struct DatabaseEditor { + database: MutexDatabase, + pub cell_cache: CellCache, + database_views: Arc, + row_event_tx: RowEventSender, +} + +impl DatabaseEditor { + pub async fn new( + database: MutexDatabase, + task_scheduler: Arc>, + ) -> FlowyResult { + let cell_cache = AnyTypeCache::::new(); + let (row_event_tx, row_event_rx) = broadcast::channel(100); + let database_view_data = Arc::new(DatabaseViewDataImpl { + database: database.clone(), + task_scheduler: task_scheduler.clone(), + cell_cache: cell_cache.clone(), + }); + + let database_views = Arc::new( + DatabaseViews::new( + database.clone(), + cell_cache.clone(), + database_view_data, + row_event_rx, + ) + .await?, + ); + Ok(Self { + database, + cell_cache, + database_views, + row_event_tx, + }) + } + + #[tracing::instrument(level = "debug", skip_all)] + pub async fn close_view_editor(&self, view_id: &str) -> bool { + self.database_views.close_view(view_id).await + } + + pub async fn close(&self) {} + + pub async fn subscribe_view_changed( + &self, + view_id: &str, + ) -> FlowyResult> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + Ok(view_editor.notifier.subscribe()) + } + + pub fn get_field(&self, field_id: &str) -> Option { + self.database.lock().fields.get_field(field_id) + } + + pub async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> { + if let Some(field) = self.database.lock().fields.get_field(¶ms.field_id) { + let group_setting = default_group_setting(&field); + self + .database + .lock() + .insert_group_setting(¶ms.view_id, group_setting); + } + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + view_editor.v_initialize_new_group(params).await?; + Ok(()) + } + + pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> { + self + .database + .lock() + .delete_group_setting(¶ms.view_id, ¶ms.group_id); + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + view_editor.v_delete_group(params).await?; + + Ok(()) + } + + pub async fn create_or_update_filter(&self, params: AlterFilterParams) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + view_editor.v_insert_filter(params).await?; + Ok(()) + } + + pub async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + view_editor.v_delete_filter(params).await?; + Ok(()) + } + + pub async fn create_or_update_sort(&self, params: AlterSortParams) -> FlowyResult { + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + let sort = view_editor.v_insert_sort(params).await?; + Ok(sort) + } + + pub async fn delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + view_editor.v_delete_sort(params).await?; + Ok(()) + } + + pub async fn get_all_filters(&self, view_id: &str) -> RepeatedFilterPB { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { + view_editor.v_get_all_filters().await.into() + } else { + RepeatedFilterPB { items: vec![] } + } + } + + pub async fn get_filter(&self, view_id: &str, filter_id: &str) -> Option { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { + Some(view_editor.v_get_filter(filter_id).await?) + } else { + None + } + } + pub async fn get_all_sorts(&self, view_id: &str) -> RepeatedSortPB { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { + view_editor.v_get_all_sorts().await.into() + } else { + RepeatedSortPB { items: vec![] } + } + } + + pub async fn delete_all_sorts(&self, view_id: &str) { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { + let _ = view_editor.v_delete_all_sorts().await; + } + } + + pub fn get_fields(&self, view_id: &str, field_ids: Option>) -> Vec { + self.database.lock().get_fields(view_id, field_ids) + } + + pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { + self + .database + .lock() + .fields + .update_field(¶ms.field_id, |update| { + update + .set_name_if_not_none(params.name) + .set_field_type_if_not_none(params.field_type.map(|field_type| field_type.into())) + .set_width_at_if_not_none(params.width.map(|value| value as i64)) + .set_visibility_if_not_none(params.visibility); + }); + self + .notify_did_update_database_field(¶ms.field_id) + .await?; + Ok(()) + } + + pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> { + self.database.lock().delete_field(field_id); + let database_id = { + let database = self.database.lock(); + database.delete_field(field_id); + database.get_database_id() + }; + let notified_changeset = + DatabaseFieldChangesetPB::delete(&database_id, vec![FieldIdPB::from(field_id)]); + self.notify_did_update_database(notified_changeset).await?; + Ok(()) + } + + pub async fn update_field_type_option( + &self, + view_id: &str, + field_id: &str, + type_option_data: TypeOptionData, + old_field: Field, + ) -> FlowyResult<()> { + let field_type = FieldType::from(old_field.field_type); + self + .database + .lock() + .fields + .update_field(field_id, |update| { + update.update_type_options(|type_options_update| { + type_options_update.insert(&field_type.to_string(), type_option_data); + }); + }); + self + .database_views + .did_update_field_type_option(view_id, field_id, &old_field) + .await?; + let _ = self.notify_did_update_database_field(field_id).await; + Ok(()) + } + + pub async fn switch_to_field_type( + &self, + field_id: &str, + new_field_type: &FieldType, + ) -> FlowyResult<()> { + let field = self.database.lock().fields.get_field(field_id); + match field { + None => {}, + Some(field) => { + let old_field_type = FieldType::from(field.field_type); + let old_type_option = field.get_any_type_option(old_field_type.clone()); + let new_type_option = field + .get_any_type_option(new_field_type) + .unwrap_or_else(|| default_type_option_data_for_type(new_field_type)); + + let transformed_type_option = transform_type_option( + &new_type_option, + new_field_type, + old_type_option, + old_field_type, + ); + self + .database + .lock() + .fields + .update_field(field_id, |update| { + update + .set_field_type(new_field_type.into()) + .set_type_option(new_field_type.into(), Some(transformed_type_option)); + }); + }, + } + + self.notify_did_update_database_field(field_id).await?; + Ok(()) + } + + pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { + let value = self + .database + .lock() + .duplicate_field(view_id, field_id, |field| format!("{} (copy)", field.name)); + if let Some((index, duplicated_field)) = value { + let _ = self + .notify_did_insert_database_field(duplicated_field, index) + .await; + } + Ok(()) + } + + pub async fn duplicate_row(&self, view_id: &str, row_id: RowId) { + let _ = self.database.lock().duplicate_row(view_id, row_id); + } + + pub async fn move_row(&self, _view_id: &str, _from: RowId, _to: RowId) { + // self.database.lock().views.update_view(view_id, |view| { + // view.move_row_order(from as u32, to as u32); + // }); + // self.row_event_tx.send(DatabaseRowEvent::Move { from: _from, to: _to}) + } + + pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult> { + let fields = self.database.lock().get_fields(¶ms.view_id, None); + let mut cells = + CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), fields).build(); + for view in self.database_views.editors().await { + view.v_will_create_row(&mut cells, ¶ms.group_id).await; + } + + let result = self.database.lock().create_row_in_view( + ¶ms.view_id, + collab_database::rows::CreateRowParams { + id: gen_row_id(), + cells, + height: 60, + visibility: true, + prev_row_id: params.start_row_id, + timestamp: timestamp(), + }, + ); + + if let Some((index, row_order)) = result { + let _ = self + .row_event_tx + .send(DatabaseRowEvent::InsertRow(InsertedRow { + row: row_order.clone(), + index: Some(index as i32), + is_new: true, + })); + + let row = self.database.lock().get_row(row_order.id); + if let Some(row) = row { + for view in self.database_views.editors().await { + view.v_did_create_row(&row, ¶ms.group_id, index).await; + } + return Ok(Some(row)); + } + } + + Ok(None) + } + + pub async fn get_field_type_option_data(&self, field_id: &str) -> Option<(Field, Bytes)> { + let field = self.database.lock().fields.get_field(field_id); + field.map(|field| { + let field_type = FieldType::from(field.field_type); + let type_option = field + .get_any_type_option(field_type.clone()) + .unwrap_or_else(|| default_type_option_data_from_type(&field_type)); + (field, type_option_to_pb(type_option, &field_type)) + }) + } + + pub async fn create_field_with_type_option( + &self, + view_id: &str, + field_type: &FieldType, + type_option_data: Option>, + ) -> (Field, Bytes) { + let name = field_type.default_name(); + let type_option_data = match type_option_data { + None => default_type_option_data_for_type(field_type), + Some(type_option_data) => type_option_data_from_pb_or_default(type_option_data, field_type), + }; + let (index, field) = + self + .database + .lock() + .create_default_field(view_id, name, field_type.into(), |field| { + field + .type_options + .insert(field_type.to_string(), type_option_data.clone()); + }); + + let _ = self + .notify_did_insert_database_field(field.clone(), index) + .await; + + (field, type_option_to_pb(type_option_data, field_type)) + } + + pub async fn move_field( + &self, + view_id: &str, + field_id: &str, + from: i32, + to: i32, + ) -> FlowyResult<()> { + let (database_id, field) = { + let database = self.database.lock(); + database.views.update_view(view_id, |view_update| { + view_update.move_field_order(from as u32, to as u32); + }); + let field = database.fields.get_field(field_id); + let database_id = database.get_database_id(); + (database_id, field) + }; + + if let Some(field) = field { + let delete_field = FieldIdPB::from(field_id); + let insert_field = IndexFieldPB::from_field(field, to as usize); + let notified_changeset = DatabaseFieldChangesetPB { + view_id: database_id, + inserted_fields: vec![insert_field], + deleted_fields: vec![delete_field], + updated_fields: vec![], + }; + + self.notify_did_update_database(notified_changeset).await?; + } + Ok(()) + } + + pub async fn get_rows(&self, view_id: &str) -> FlowyResult>> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + Ok(view_editor.v_get_rows().await) + } + + pub fn get_row(&self, row_id: RowId) -> Option { + self.database.lock().get_row(row_id) + } + + pub async fn delete_row(&self, row_id: RowId) { + let row = self.database.lock().remove_row(row_id); + if let Some(row) = row { + tracing::trace!("Did delete row:{:?}", row); + let _ = self + .row_event_tx + .send(DatabaseRowEvent::DeleteRow(row.id.into())); + + for view in self.database_views.editors().await { + view.v_did_delete_row(&row).await; + } + } + } + + pub async fn get_cell(&self, field_id: &str, row_id: RowId) -> CellPB { + let field = self.database.lock().fields.get_field(field_id); + let cell = self.database.lock().get_cell(field_id, row_id); + match (field, cell) { + (Some(field), Some(cell)) => { + let field_type = FieldType::from(field.field_type); + let cell_bytes = get_type_cell_protobuf(&cell, &field, Some(self.cell_cache.clone())); + CellPB { + field_id: field_id.to_string(), + row_id: row_id.into(), + data: cell_bytes.to_vec(), + field_type: Some(field_type), + } + }, + _ => CellPB::empty(field_id, row_id.into()), + } + } + + pub async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec { + self.database.lock().get_cells_for_field(view_id, field_id) + } + + pub async fn update_cell_with_changeset( + &self, + view_id: &str, + row_id: RowId, + field_id: &str, + cell_changeset: T, + ) -> Option<()> + where + T: ToCellChangeset, + { + let (field, cell) = { + let database = self.database.lock(); + ( + database.fields.get_field(field_id)?, + database.get_cell(field_id, row_id).map(|cell| cell.cell), + ) + }; + let cell_changeset = cell_changeset.to_cell_changeset_str(); + let new_cell = + apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone())); + self.update_cell(view_id, row_id, field_id, new_cell).await + } + + pub async fn update_cell( + &self, + view_id: &str, + row_id: RowId, + field_id: &str, + new_cell: Cell, + ) -> Option<()> { + let old_row = { + let database = self.database.lock(); + database.get_row(row_id) + }; + self.database.lock().update_row(row_id, |row_update| { + row_update.update_cells(|cell_update| { + cell_update.insert(field_id, new_cell); + }); + }); + + let option_row = self.database.lock().get_row(row_id); + if let Some(new_row) = option_row { + let _ = self + .row_event_tx + .send(DatabaseRowEvent::UpdateRow(UpdatedRow { + row: RowOrder::from(&new_row), + field_ids: vec![field_id.to_string()], + })); + for view in self.database_views.editors().await { + view.v_did_update_row(&old_row, &new_row).await; + } + } + + notify_did_update_cell(vec![CellChangesetNotifyPB { + view_id: view_id.to_string(), + row_id: row_id.into(), + field_id: field_id.to_string(), + }]) + .await; + None + } + + pub async fn create_select_option( + &self, + field_id: &str, + option_name: String, + ) -> Option { + let field = self.database.lock().fields.get_field(field_id)?; + let type_option = select_type_option_from_field(&field).ok()?; + let select_option = type_option.create_option(&option_name); + Some(SelectOptionPB::from(select_option)) + } + + pub async fn insert_select_options( + &self, + view_id: &str, + field_id: &str, + row_id: RowId, + options: Vec, + ) -> Option<()> { + let field = self.database.lock().fields.get_field(field_id)?; + let mut type_option = select_type_option_from_field(&field).ok()?; + let cell_changeset = SelectOptionCellChangeset { + insert_option_ids: options.iter().map(|option| option.id.clone()).collect(), + ..Default::default() + }; + + for option in options { + type_option.insert_option(option.into()); + } + self + .database + .lock() + .fields + .update_field(field_id, |update| { + update.set_type_option(field.field_type, Some(type_option.to_type_option_data())); + }); + + self + .update_cell_with_changeset(view_id, row_id, field_id, cell_changeset) + .await; + None + } + + pub async fn delete_select_options( + &self, + view_id: &str, + field_id: &str, + row_id: RowId, + options: Vec, + ) -> Option<()> { + let field = self.database.lock().fields.get_field(field_id)?; + let mut type_option = select_type_option_from_field(&field).ok()?; + let cell_changeset = SelectOptionCellChangeset { + delete_option_ids: options.iter().map(|option| option.id.clone()).collect(), + ..Default::default() + }; + + for option in options { + type_option.delete_option(option.into()); + } + self + .database + .lock() + .fields + .update_field(field_id, |update| { + update.set_type_option(field.field_type, Some(type_option.to_type_option_data())); + }); + + self + .update_cell_with_changeset(view_id, row_id, field_id, cell_changeset) + .await; + None + } + + pub async fn get_select_options(&self, row_id: RowId, field_id: &str) -> SelectOptionCellDataPB { + let field = self.database.lock().fields.get_field(field_id); + match field { + None => SelectOptionCellDataPB::default(), + Some(field) => { + let row_cell = self.database.lock().get_cell(field_id, row_id); + let ids = match row_cell { + None => SelectOptionIds::new(), + Some(row_cell) => SelectOptionIds::from(&row_cell.cell), + }; + match select_type_option_from_field(&field) { + Ok(type_option) => type_option.get_selected_options(ids).into(), + Err(_) => SelectOptionCellDataPB::default(), + } + }, + } + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn load_groups(&self, view_id: &str) -> FlowyResult { + let view = self.database_views.get_view_editor(view_id).await?; + let groups = view.v_load_groups().await?; + Ok(RepeatedGroupPB { items: groups }) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult { + let view = self.database_views.get_view_editor(view_id).await?; + let group = view.v_get_group(group_id).await?; + Ok(group) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn move_group( + &self, + view_id: &str, + from_group: &str, + to_group: &str, + ) -> FlowyResult<()> { + let view = self.database_views.get_view_editor(view_id).await?; + view.v_move_group(from_group, to_group).await?; + Ok(()) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn move_group_row( + &self, + view_id: &str, + to_group: &str, + from_row: RowId, + to_row: Option, + ) -> FlowyResult<()> { + let row = self.database.lock().get_row(from_row); + match row { + None => { + tracing::warn!( + "Move row between group failed, can not find the row:{}", + from_row + ) + }, + Some(row) => { + let mut row_changeset = RowChangeset::new(row.id); + let view = self.database_views.get_view_editor(view_id).await?; + view + .v_move_group_row(&row, &mut row_changeset, to_group, to_row) + .await; + + tracing::trace!("Row data changed: {:?}", row_changeset); + self.database.lock().update_row(row.id, |row| { + row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone())); + }); + + let cell_changesets = cell_changesets_from_cell_by_field_id( + view_id, + row_changeset.row_id, + row_changeset.cell_by_field_id, + ); + notify_did_update_cell(cell_changesets).await; + }, + } + + Ok(()) + } + + pub async fn group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { + let view = self.database_views.get_view_editor(view_id).await?; + view.v_update_group_setting(field_id).await?; + Ok(()) + } + + pub async fn set_layout_setting( + &self, + view_id: &str, + layout_ty: DatabaseLayout, + layout_setting: LayoutSettingParams, + ) { + if let Ok(view) = self.database_views.get_view_editor(view_id).await { + let _ = view.v_set_layout_settings(&layout_ty, layout_setting).await; + } + } + + pub async fn get_layout_setting( + &self, + view_id: &str, + layout_ty: DatabaseLayout, + ) -> Option { + let view = self.database_views.get_view_editor(view_id).await.ok()?; + let layout_setting = view.v_get_layout_settings(&layout_ty).await; + Some(layout_setting) + } + + #[tracing::instrument(level = "trace", skip_all)] + pub async fn get_all_calendar_events(&self, view_id: &str) -> Vec { + match self.database_views.get_view_editor(view_id).await { + Ok(view) => view.v_get_all_calendar_events().await.unwrap_or_default(), + Err(_) => { + tracing::warn!("Can not find the view: {}", view_id); + vec![] + }, + } + } + + #[tracing::instrument(level = "trace", skip_all)] + pub async fn get_calendar_event(&self, view_id: &str, row_id: RowId) -> Option { + let view = self.database_views.get_view_editor(view_id).await.ok()?; + view.v_get_calendar_event(row_id).await + } + + #[tracing::instrument(level = "trace", skip_all, err)] + async fn notify_did_insert_database_field(&self, field: Field, index: usize) -> FlowyResult<()> { + let database_id = self.database.lock().get_database_id(); + let index_field = IndexFieldPB::from_field(field, index); + let notified_changeset = DatabaseFieldChangesetPB::insert(&database_id, vec![index_field]); + let _ = self.notify_did_update_database(notified_changeset).await; + Ok(()) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + async fn notify_did_update_database_field(&self, field_id: &str) -> FlowyResult<()> { + let (database_id, field) = { + let database = self.database.lock(); + let database_id = database.get_database_id(); + let field = database.fields.get_field(field_id); + (database_id, field) + }; + + if let Some(field) = field { + let updated_field = FieldPB::from(field); + let notified_changeset = + DatabaseFieldChangesetPB::update(&database_id, vec![updated_field.clone()]); + self.notify_did_update_database(notified_changeset).await?; + send_notification(field_id, DatabaseNotification::DidUpdateField) + .payload(updated_field) + .send(); + } + + Ok(()) + } + + async fn notify_did_update_database( + &self, + changeset: DatabaseFieldChangesetPB, + ) -> FlowyResult<()> { + let views = self.database.lock().get_all_views_description(); + for view in views { + send_notification(&view.id, DatabaseNotification::DidUpdateFields) + .payload(changeset.clone()) + .send(); + } + + Ok(()) + } + + pub async fn get_database_view_setting( + &self, + view_id: &str, + ) -> FlowyResult { + let view = self + .database + .lock() + .get_view(view_id) + .ok_or_else(|| FlowyError::record_not_found().context("Can't find the database view"))?; + Ok(database_view_setting_pb_from_view(view)) + } + + pub async fn get_database_data(&self, view_id: &str) -> DatabasePB { + let rows = self.get_rows(view_id).await.unwrap_or_default(); + let (database_id, fields) = { + let database = self.database.lock(); + let database_id = database.get_database_id(); + let fields = database + .fields + .get_all_field_orders() + .into_iter() + .map(FieldIdPB::from) + .collect(); + (database_id, fields) + }; + + let rows = rows + .into_iter() + .map(|row| RowPB::from(row.as_ref())) + .collect::>(); + DatabasePB { + id: database_id, + fields, + rows, + } + } +} + +pub(crate) async fn notify_did_update_cell(changesets: Vec) { + for changeset in changesets { + let id = format!("{}:{}", changeset.row_id, changeset.field_id); + send_notification(&id, DatabaseNotification::DidUpdateCell).send(); + } +} + +fn cell_changesets_from_cell_by_field_id( + view_id: &str, + row_id: RowId, + cell_by_field_id: HashMap, +) -> Vec { + let row_id = row_id.into(); + cell_by_field_id + .into_iter() + .map(|(field_id, _cell)| CellChangesetNotifyPB { + view_id: view_id.to_string(), + row_id, + field_id, + }) + .collect() +} + +#[derive(Clone)] +pub struct MutexDatabase(Arc>>); + +impl MutexDatabase { + pub(crate) fn new(database: Arc) -> Self { + Self(Arc::new(Mutex::new(database))) + } +} + +impl Deref for MutexDatabase { + type Target = Arc>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +unsafe impl Sync for MutexDatabase {} + +unsafe impl Send for MutexDatabase {} + +struct DatabaseViewDataImpl { + database: MutexDatabase, + task_scheduler: Arc>, + cell_cache: CellCache, +} + +impl DatabaseViewData for DatabaseViewDataImpl { + fn get_view_setting(&self, view_id: &str) -> Fut> { + let view = self.database.lock().get_view(view_id); + to_fut(async move { view }) + } + + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { + let fields = self.database.lock().get_fields(view_id, field_ids); + to_fut(async move { fields.into_iter().map(Arc::new).collect() }) + } + + fn get_field(&self, field_id: &str) -> Fut>> { + let field = self + .database + .lock() + .fields + .get_field(field_id) + .map(Arc::new); + to_fut(async move { field }) + } + + fn get_primary_field(&self) -> Fut>> { + let field = self + .database + .lock() + .fields + .get_primary_field() + .map(Arc::new); + to_fut(async move { field }) + } + + fn index_of_row(&self, view_id: &str, row_id: RowId) -> Fut> { + let index = self.database.lock().index_of_row(view_id, row_id); + to_fut(async move { index }) + } + + fn get_row(&self, view_id: &str, row_id: RowId) -> Fut)>> { + let index = self.database.lock().index_of_row(view_id, row_id); + let row = self.database.lock().get_row(row_id); + to_fut(async move { + match (index, row) { + (Some(index), Some(row)) => Some((index, Arc::new(row))), + _ => None, + } + }) + } + + fn get_rows(&self, view_id: &str) -> Fut>> { + let rows = self.database.lock().get_rows_for_view(view_id); + to_fut(async move { rows.into_iter().map(Arc::new).collect() }) + } + + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>> { + let cells = self.database.lock().get_cells_for_field(view_id, field_id); + to_fut(async move { cells.into_iter().map(Arc::new).collect() }) + } + + fn get_cell_in_row(&self, field_id: &str, row_id: RowId) -> Fut>> { + let cell = self.database.lock().get_cell(field_id, row_id); + to_fut(async move { cell.map(Arc::new) }) + } + + fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout { + self + .database + .lock() + .views + .get_view_layout(view_id) + .unwrap_or_default() + } + + fn get_group_setting(&self, view_id: &str) -> Vec { + self.database.lock().get_all_group_setting(view_id) + } + + fn insert_group_setting(&self, view_id: &str, setting: GroupSetting) { + self.database.lock().insert_group_setting(view_id, setting); + } + + fn get_sort(&self, view_id: &str, sort_id: &str) -> Option { + self.database.lock().get_sort::(view_id, sort_id) + } + + fn insert_sort(&self, view_id: &str, sort: Sort) { + self.database.lock().insert_sort(view_id, sort); + } + + fn remove_sort(&self, view_id: &str, sort_id: &str) { + self.database.lock().remove_sort(view_id, sort_id); + } + + fn get_all_sorts(&self, view_id: &str) -> Vec { + self.database.lock().get_all_sorts::(view_id) + } + + fn remove_all_sorts(&self, view_id: &str) { + self.database.lock().remove_all_sorts(view_id); + } + + fn get_all_filters(&self, view_id: &str) -> Vec> { + self + .database + .lock() + .get_all_filters(view_id) + .into_iter() + .map(Arc::new) + .collect() + } + + fn delete_filter(&self, view_id: &str, filter_id: &str) { + self.database.lock().remove_filter(view_id, filter_id); + } + + fn insert_filter(&self, view_id: &str, filter: Filter) { + self.database.lock().insert_filter(view_id, filter); + } + + fn get_filter(&self, view_id: &str, filter_id: &str) -> Option { + self + .database + .lock() + .get_filter::(view_id, filter_id) + } + + fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option { + self + .database + .lock() + .get_filter_by_field_id::(view_id, field_id) + } + + fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option { + self + .database + .lock() + .views + .get_layout_setting(view_id, layout_ty) + } + + fn insert_layout_setting( + &self, + view_id: &str, + layout_ty: &DatabaseLayout, + layout_setting: LayoutSetting, + ) { + self + .database + .lock() + .insert_layout_setting(view_id, layout_ty, layout_setting); + } + + fn get_task_scheduler(&self) -> Arc> { + self.task_scheduler.clone() + } + + fn get_type_option_cell_handler( + &self, + field: &Field, + field_type: &FieldType, + ) -> Option> { + TypeOptionCellExt::new_with_cell_data_cache(field, Some(self.cell_cache.clone())) + .get_type_option_cell_data_handler(field_type) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database/entities.rs b/frontend/rust-lib/flowy-database2/src/services/database/entities.rs new file mode 100644 index 0000000000..acaeb4b2e1 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database/entities.rs @@ -0,0 +1,26 @@ +use collab_database::views::RowOrder; + +#[derive(Debug, Clone)] +pub enum DatabaseRowEvent { + InsertRow(InsertedRow), + UpdateRow(UpdatedRow), + DeleteRow(i64), + Move { + deleted_row_id: i64, + inserted_row: InsertedRow, + }, +} + +#[derive(Debug, Clone)] +pub struct InsertedRow { + pub row: RowOrder, + pub index: Option, + pub is_new: bool, +} + +#[derive(Debug, Clone)] +pub struct UpdatedRow { + pub row: RowOrder, + // represents as the cells that were updated in this row. + pub field_ids: Vec, +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database/mod.rs b/frontend/rust-lib/flowy-database2/src/services/database/mod.rs new file mode 100644 index 0000000000..37cb768ddb --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database/mod.rs @@ -0,0 +1,7 @@ +mod database_editor; +mod entities; +mod util; + +pub use database_editor::*; +pub use entities::*; +pub(crate) use util::database_view_setting_pb_from_view; diff --git a/frontend/rust-lib/flowy-database2/src/services/database/util.rs b/frontend/rust-lib/flowy-database2/src/services/database/util.rs new file mode 100644 index 0000000000..f47d1b7f59 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database/util.rs @@ -0,0 +1,56 @@ +use crate::entities::{ + CalendarLayoutSettingPB, DatabaseLayoutPB, DatabaseViewSettingPB, FilterPB, GroupSettingPB, + LayoutSettingPB, SortPB, +}; +use crate::services::filter::Filter; +use crate::services::group::GroupSetting; +use crate::services::setting::CalendarLayoutSetting; +use crate::services::sort::Sort; +use collab_database::views::DatabaseView; + +pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> DatabaseViewSettingPB { + let layout_setting = if let Some(layout_setting) = view.layout_settings.get(&view.layout) { + let calendar_setting = + CalendarLayoutSettingPB::from(CalendarLayoutSetting::from(layout_setting.clone())); + LayoutSettingPB { + calendar: Some(calendar_setting), + } + } else { + LayoutSettingPB::default() + }; + + let current_layout: DatabaseLayoutPB = view.layout.into(); + let filters = view + .filters + .into_iter() + .flat_map(|value| match Filter::try_from(value) { + Ok(filter) => Some(FilterPB::from(&filter)), + Err(_) => None, + }) + .collect::>(); + let group_settings = view + .group_settings + .into_iter() + .flat_map(|value| match GroupSetting::try_from(value) { + Ok(setting) => Some(GroupSettingPB::from(&setting)), + Err(_) => None, + }) + .collect::>(); + + let sorts = view + .sorts + .into_iter() + .flat_map(|value| match Sort::try_from(value) { + Ok(sort) => Some(SortPB::from(&sort)), + Err(_) => None, + }) + .collect::>(); + + DatabaseViewSettingPB { + current_layout, + filters: filters.into(), + group_settings: group_settings.into(), + sorts: sorts.into(), + layout_setting, + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs new file mode 100644 index 0000000000..9ee645a7cb --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs @@ -0,0 +1,11 @@ +mod notifier; +mod view_editor; +mod view_filter; +mod view_group; +mod view_sort; +mod views; +// mod trait_impl; + +pub use notifier::*; +pub use view_editor::*; +pub use views::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs new file mode 100644 index 0000000000..48e05a017c --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs @@ -0,0 +1,111 @@ +#![allow(clippy::while_let_loop)] +use crate::entities::{ + DatabaseViewSettingPB, FilterChangesetNotificationPB, GroupChangesetPB, GroupRowsNotificationPB, + ReorderAllRowsPB, ReorderSingleRowPB, RowsVisibilityChangesetPB, SortChangesetNotificationPB, +}; +use crate::notification::{send_notification, DatabaseNotification}; +use crate::services::filter::FilterResultNotification; +use crate::services::sort::{ReorderAllRowsResult, ReorderSingleRowResult}; +use async_stream::stream; +use futures::stream::StreamExt; +use tokio::sync::broadcast; + +#[derive(Clone)] +pub enum DatabaseViewChanged { + FilterNotification(FilterResultNotification), + ReorderAllRowsNotification(ReorderAllRowsResult), + ReorderSingleRowNotification(ReorderSingleRowResult), +} + +pub type DatabaseViewChangedNotifier = broadcast::Sender; + +pub(crate) struct DatabaseViewChangedReceiverRunner( + pub(crate) Option>, +); + +impl DatabaseViewChangedReceiverRunner { + pub(crate) async fn run(mut self) { + let mut receiver = self.0.take().expect("Only take once"); + let stream = stream! { + loop { + match receiver.recv().await { + Ok(changed) => yield changed, + Err(_e) => break, + } + } + }; + stream + .for_each(|changed| async { + match changed { + DatabaseViewChanged::FilterNotification(notification) => { + let changeset = RowsVisibilityChangesetPB { + view_id: notification.view_id, + visible_rows: notification.visible_rows, + invisible_rows: notification.invisible_rows, + }; + + send_notification( + &changeset.view_id, + DatabaseNotification::DidUpdateViewRowsVisibility, + ) + .payload(changeset) + .send() + }, + DatabaseViewChanged::ReorderAllRowsNotification(notification) => { + let row_orders = ReorderAllRowsPB { + row_orders: notification.row_orders, + }; + send_notification(¬ification.view_id, DatabaseNotification::DidReorderRows) + .payload(row_orders) + .send() + }, + DatabaseViewChanged::ReorderSingleRowNotification(notification) => { + let reorder_row = ReorderSingleRowPB { + row_id: notification.row_id, + old_index: notification.old_index as i32, + new_index: notification.new_index as i32, + }; + send_notification( + ¬ification.view_id, + DatabaseNotification::DidReorderSingleRow, + ) + .payload(reorder_row) + .send() + }, + } + }) + .await; + } +} + +pub async fn notify_did_update_group_rows(payload: GroupRowsNotificationPB) { + send_notification(&payload.group_id, DatabaseNotification::DidUpdateGroupRow) + .payload(payload) + .send(); +} + +pub async fn notify_did_update_filter(notification: FilterChangesetNotificationPB) { + send_notification(¬ification.view_id, DatabaseNotification::DidUpdateFilter) + .payload(notification) + .send(); +} + +pub async fn notify_did_update_sort(notification: SortChangesetNotificationPB) { + if !notification.is_empty() { + send_notification(¬ification.view_id, DatabaseNotification::DidUpdateSort) + .payload(notification) + .send(); + } +} + +pub(crate) async fn notify_did_update_groups(view_id: &str, changeset: GroupChangesetPB) { + send_notification(view_id, DatabaseNotification::DidUpdateGroups) + .payload(changeset) + .send(); +} + +pub(crate) async fn notify_did_update_setting(view_id: &str, setting: DatabaseViewSettingPB) { + send_notification(view_id, DatabaseNotification::DidUpdateSettings) + .payload(setting) + .send(); +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs new file mode 100644 index 0000000000..615d8fbc29 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -0,0 +1,791 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::Arc; + +use collab_database::database::{gen_database_filter_id, gen_database_sort_id}; +use collab_database::fields::Field; +use collab_database::rows::{Cells, Row, RowCell, RowId}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; +use tokio::sync::{broadcast, RwLock}; + +use flowy_error::{FlowyError, FlowyResult}; +use flowy_task::TaskDispatcher; +use lib_infra::future::Fut; + +use crate::entities::{ + AlterFilterParams, AlterSortParams, CalendarEventPB, DeleteFilterParams, DeleteGroupParams, + DeleteSortParams, FieldType, GroupChangesetPB, GroupPB, GroupRowsNotificationPB, + InsertGroupParams, InsertedGroupPB, InsertedRowPB, LayoutSettingPB, LayoutSettingParams, RowPB, + RowsChangesetPB, SortChangesetNotificationPB, SortPB, +}; +use crate::notification::{send_notification, DatabaseNotification}; +use crate::services::cell::CellCache; +use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent}; +use crate::services::database_view::view_filter::make_filter_controller; +use crate::services::database_view::view_group::{ + get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field, +}; +use crate::services::database_view::view_sort::make_sort_controller; +use crate::services::database_view::{ + notify_did_update_filter, notify_did_update_group_rows, notify_did_update_groups, + notify_did_update_setting, notify_did_update_sort, DatabaseViewChangedNotifier, + DatabaseViewChangedReceiverRunner, +}; +use crate::services::field::TypeOptionCellDataHandler; +use crate::services::filter::{ + Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType, +}; +use crate::services::group::{GroupController, GroupSetting, MoveGroupRowContext, RowChangeset}; +use crate::services::setting::CalendarLayoutSetting; +use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; + +pub trait DatabaseViewData: Send + Sync + 'static { + fn get_view_setting(&self, view_id: &str) -> Fut>; + /// If the field_ids is None, then it will return all the field revisions + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; + + /// Returns the field with the field_id + fn get_field(&self, field_id: &str) -> Fut>>; + + fn get_primary_field(&self) -> Fut>>; + + /// Returns the index of the row with row_id + fn index_of_row(&self, view_id: &str, row_id: RowId) -> Fut>; + + /// Returns the `index` and `RowRevision` with row_id + fn get_row(&self, view_id: &str, row_id: RowId) -> Fut)>>; + + fn get_rows(&self, view_id: &str) -> Fut>>; + + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; + + fn get_cell_in_row(&self, field_id: &str, row_id: RowId) -> Fut>>; + + fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; + + fn get_group_setting(&self, view_id: &str) -> Vec; + + fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); + + fn get_sort(&self, view_id: &str, sort_id: &str) -> Option; + + fn insert_sort(&self, view_id: &str, sort: Sort); + + fn remove_sort(&self, view_id: &str, sort_id: &str); + + fn get_all_sorts(&self, view_id: &str) -> Vec; + + fn remove_all_sorts(&self, view_id: &str); + + fn get_all_filters(&self, view_id: &str) -> Vec>; + + fn delete_filter(&self, view_id: &str, filter_id: &str); + + fn insert_filter(&self, view_id: &str, filter: Filter); + + fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; + + fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option; + + fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option; + + fn insert_layout_setting( + &self, + view_id: &str, + layout_ty: &DatabaseLayout, + layout_setting: LayoutSetting, + ); + + /// Returns a `TaskDispatcher` used to poll a `Task` + fn get_task_scheduler(&self) -> Arc>; + + fn get_type_option_cell_handler( + &self, + field: &Field, + field_type: &FieldType, + ) -> Option>; +} + +pub struct DatabaseViewEditor { + pub view_id: String, + delegate: Arc, + group_controller: Arc>>, + filter_controller: Arc, + sort_controller: Arc>, + pub notifier: DatabaseViewChangedNotifier, +} + +impl Drop for DatabaseViewEditor { + fn drop(&mut self) { + tracing::trace!("Drop {}", std::any::type_name::()); + } +} + +impl DatabaseViewEditor { + pub async fn new( + view_id: String, + delegate: Arc, + cell_cache: CellCache, + ) -> FlowyResult { + let (notifier, _) = broadcast::channel(100); + tokio::spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); + let group_controller = new_group_controller(view_id.clone(), delegate.clone()).await?; + let group_controller = Arc::new(RwLock::new(group_controller)); + + let filter_controller = make_filter_controller( + &view_id, + delegate.clone(), + notifier.clone(), + cell_cache.clone(), + ) + .await; + + let sort_controller = make_sort_controller( + &view_id, + delegate.clone(), + notifier.clone(), + filter_controller.clone(), + cell_cache, + ) + .await; + + Ok(Self { + view_id, + delegate, + group_controller, + filter_controller, + sort_controller, + notifier, + }) + } + + pub async fn close(&self) { + self.sort_controller.write().await.close().await; + self.filter_controller.close().await; + } + + pub async fn v_will_create_row(&self, cells: &mut Cells, group_id: &Option) { + if group_id.is_none() { + return; + } + let group_id = group_id.as_ref().unwrap(); + let _ = self + .mut_group_controller(|group_controller, field| { + group_controller.will_create_row(cells, &field, group_id); + Ok(()) + }) + .await; + } + + pub async fn v_did_create_row(&self, row: &Row, group_id: &Option, index: usize) { + // Send the group notification if the current view has groups + match group_id.as_ref() { + None => {}, + Some(group_id) => { + self + .group_controller + .write() + .await + .did_create_row(row, group_id); + let inserted_row = InsertedRowPB { + row: RowPB::from(row), + index: Some(index as i32), + is_new: true, + }; + let changeset = GroupRowsNotificationPB::insert(group_id.clone(), vec![inserted_row]); + notify_did_update_group_rows(changeset).await; + }, + } + } + + #[tracing::instrument(level = "trace", skip_all)] + pub async fn v_did_delete_row(&self, row: &Row) { + // Send the group notification if the current view has groups; + let result = self + .mut_group_controller(|group_controller, field| { + group_controller.did_delete_delete_row(row, &field) + }) + .await; + + if let Some(result) = result { + tracing::trace!("Delete row in view changeset: {:?}", result.row_changesets); + for changeset in result.row_changesets { + notify_did_update_group_rows(changeset).await; + } + } + } + + pub async fn v_did_update_row(&self, old_row: &Option, row: &Row) { + let result = self + .mut_group_controller(|group_controller, field| { + Ok(group_controller.did_update_group_row(old_row, row, &field)) + }) + .await; + + if let Some(Ok(result)) = result { + let mut changeset = GroupChangesetPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + if let Some(inserted_group) = result.inserted_group { + tracing::trace!("Create group after editing the row: {:?}", inserted_group); + changeset.inserted_groups.push(inserted_group); + } + if let Some(delete_group) = result.deleted_group { + tracing::trace!("Delete group after editing the row: {:?}", delete_group); + changeset.deleted_groups.push(delete_group.group_id); + } + notify_did_update_groups(&self.view_id, changeset).await; + + tracing::trace!( + "Group changesets after editing the row: {:?}", + result.row_changesets + ); + for changeset in result.row_changesets { + notify_did_update_group_rows(changeset).await; + } + } + + let filter_controller = self.filter_controller.clone(); + let sort_controller = self.sort_controller.clone(); + let row_id = row.id; + tokio::spawn(async move { + filter_controller.did_receive_row_changed(row_id).await; + sort_controller + .read() + .await + .did_receive_row_changed(row_id) + .await; + }); + } + + pub async fn v_filter_rows(&self, rows: &mut Vec>) { + self.filter_controller.filter_rows(rows).await + } + + pub async fn v_sort_rows(&self, rows: &mut Vec>) { + self.sort_controller.write().await.sort_rows(rows).await + } + + pub async fn v_get_rows(&self) -> Vec> { + let mut rows = self.delegate.get_rows(&self.view_id).await; + self.v_filter_rows(&mut rows).await; + self.v_sort_rows(&mut rows).await; + rows + } + + pub async fn v_move_group_row( + &self, + row: &Row, + row_changeset: &mut RowChangeset, + to_group_id: &str, + to_row_id: Option, + ) { + let result = self + .mut_group_controller(|group_controller, field| { + let move_row_context = MoveGroupRowContext { + row, + row_changeset, + field: field.as_ref(), + to_group_id, + to_row_id, + }; + group_controller.move_group_row(move_row_context) + }) + .await; + + if let Some(result) = result { + let mut changeset = GroupChangesetPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + if let Some(delete_group) = result.deleted_group { + tracing::info!("Delete group after moving the row: {:?}", delete_group); + changeset.deleted_groups.push(delete_group.group_id); + } + notify_did_update_groups(&self.view_id, changeset).await; + + for changeset in result.row_changesets { + notify_did_update_group_rows(changeset).await; + } + } + } + /// Only call once after database view editor initialized + #[tracing::instrument(level = "trace", skip(self))] + pub async fn v_load_groups(&self) -> FlowyResult> { + let groups = self + .group_controller + .read() + .await + .groups() + .into_iter() + .map(|group_data| GroupPB::from(group_data.clone())) + .collect::>(); + tracing::trace!("Number of groups: {}", groups.len()); + Ok(groups) + } + + #[tracing::instrument(level = "trace", skip(self))] + pub async fn v_get_group(&self, group_id: &str) -> FlowyResult { + match self.group_controller.read().await.get_group(group_id) { + None => Err(FlowyError::record_not_found().context("Can't find the group")), + Some((_, group)) => Ok(GroupPB::from(group)), + } + } + + #[tracing::instrument(level = "trace", skip(self), err)] + pub async fn v_move_group(&self, from_group: &str, to_group: &str) -> FlowyResult<()> { + self + .group_controller + .write() + .await + .move_group(from_group, to_group)?; + match self.group_controller.read().await.get_group(from_group) { + None => tracing::warn!("Can not find the group with id: {}", from_group), + Some((index, group)) => { + let inserted_group = InsertedGroupPB { + group: GroupPB::from(group), + index: index as i32, + }; + + let changeset = GroupChangesetPB { + view_id: self.view_id.clone(), + inserted_groups: vec![inserted_group], + deleted_groups: vec![from_group.to_string()], + update_groups: vec![], + initial_groups: vec![], + }; + + notify_did_update_groups(&self.view_id, changeset).await; + }, + } + Ok(()) + } + + pub async fn group_id(&self) -> String { + self.group_controller.read().await.field_id().to_string() + } + + pub async fn v_initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> { + if self.group_controller.read().await.field_id() != params.field_id { + self.v_update_group_setting(¶ms.field_id).await?; + + if let Some(view) = self.delegate.get_view_setting(&self.view_id).await { + let setting = database_view_setting_pb_from_view(view); + notify_did_update_setting(&self.view_id, setting).await; + } + } + Ok(()) + } + + pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> { + Ok(()) + } + + pub async fn v_get_all_sorts(&self) -> Vec { + self.delegate.get_all_sorts(&self.view_id) + } + + #[tracing::instrument(level = "trace", skip(self), err)] + pub async fn v_insert_sort(&self, params: AlterSortParams) -> FlowyResult { + let is_exist = params.sort_id.is_some(); + let sort_id = match params.sort_id { + None => gen_database_sort_id(), + Some(sort_id) => sort_id, + }; + + let sort = Sort { + id: sort_id, + field_id: params.field_id.clone(), + field_type: params.field_type, + condition: params.condition, + }; + let sort_type = SortType::from(&sort); + let mut sort_controller = self.sort_controller.write().await; + self.delegate.insert_sort(&self.view_id, sort.clone()); + let changeset = if is_exist { + sort_controller + .did_receive_changes(SortChangeset::from_update(sort_type)) + .await + } else { + sort_controller + .did_receive_changes(SortChangeset::from_insert(sort_type)) + .await + }; + drop(sort_controller); + notify_did_update_sort(changeset).await; + Ok(sort) + } + + pub async fn v_delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> { + let notification = self + .sort_controller + .write() + .await + .did_receive_changes(SortChangeset::from_delete(DeletedSortType::from( + params.clone(), + ))) + .await; + + self.delegate.remove_sort(&self.view_id, ¶ms.sort_id); + notify_did_update_sort(notification).await; + Ok(()) + } + + pub async fn v_delete_all_sorts(&self) -> FlowyResult<()> { + let all_sorts = self.v_get_all_sorts().await; + self.delegate.remove_all_sorts(&self.view_id); + + let mut notification = SortChangesetNotificationPB::new(self.view_id.clone()); + notification.delete_sorts = all_sorts.into_iter().map(SortPB::from).collect(); + notify_did_update_sort(notification).await; + Ok(()) + } + + pub async fn v_get_all_filters(&self) -> Vec> { + self.delegate.get_all_filters(&self.view_id) + } + + #[tracing::instrument(level = "trace", skip(self), err)] + pub async fn v_insert_filter(&self, params: AlterFilterParams) -> FlowyResult<()> { + let is_exist = params.filter_id.is_some(); + let filter_id = match params.filter_id { + None => gen_database_filter_id(), + Some(filter_id) => filter_id, + }; + let filter = Filter { + id: filter_id.clone(), + field_id: params.field_id.clone(), + field_type: params.field_type, + condition: params.condition, + content: params.content, + }; + let filter_type = FilterType::from(&filter); + let filter_controller = self.filter_controller.clone(); + let changeset = if is_exist { + let old_filter_type = self + .delegate + .get_filter(&self.view_id, &filter.id) + .map(|field| FilterType::from(&field)); + + self.delegate.insert_filter(&self.view_id, filter); + filter_controller + .did_receive_changes(FilterChangeset::from_update(UpdatedFilterType::new( + old_filter_type, + filter_type, + ))) + .await + } else { + self.delegate.insert_filter(&self.view_id, filter); + filter_controller + .did_receive_changes(FilterChangeset::from_insert(filter_type)) + .await + }; + drop(filter_controller); + + if let Some(changeset) = changeset { + notify_did_update_filter(changeset).await; + } + Ok(()) + } + + #[tracing::instrument(level = "trace", skip(self), err)] + pub async fn v_delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { + let filter_type = params.filter_type; + let changeset = self + .filter_controller + .did_receive_changes(FilterChangeset::from_delete(filter_type.clone())) + .await; + + self + .delegate + .delete_filter(&self.view_id, &filter_type.filter_id); + if changeset.is_some() { + notify_did_update_filter(changeset.unwrap()).await; + } + Ok(()) + } + + pub async fn v_get_filter(&self, filter_id: &str) -> Option { + self.delegate.get_filter(&self.view_id, filter_id) + } + + /// Returns the current calendar settings + #[tracing::instrument(level = "debug", skip(self))] + pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams { + let mut layout_setting = LayoutSettingParams::default(); + match layout_ty { + DatabaseLayout::Grid => {}, + DatabaseLayout::Board => {}, + DatabaseLayout::Calendar => { + if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { + let calendar_setting = CalendarLayoutSetting::from(value); + // Check the field exist or not + if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await { + let field_type = FieldType::from(field.field_type); + + // Check the type of field is Datetime or not + if field_type == FieldType::DateTime { + layout_setting.calendar = Some(calendar_setting); + } + } + } + }, + } + + tracing::debug!("{:?}", layout_setting); + layout_setting + } + + /// Update the calendar settings and send the notification to refresh the UI + pub async fn v_set_layout_settings( + &self, + _layout_ty: &DatabaseLayout, + params: LayoutSettingParams, + ) -> FlowyResult<()> { + // Maybe it needs no send notification to refresh the UI + if let Some(new_calendar_setting) = params.calendar { + if let Some(field) = self + .delegate + .get_field(&new_calendar_setting.field_id) + .await + { + let field_type = FieldType::from(field.field_type); + if field_type != FieldType::DateTime { + return Err(FlowyError::unexpect_calendar_field_type()); + } + + let layout_ty = DatabaseLayout::Calendar; + let old_calender_setting = self.v_get_layout_settings(&layout_ty).await.calendar; + + self.delegate.insert_layout_setting( + &self.view_id, + &layout_ty, + new_calendar_setting.clone().into(), + ); + let new_field_id = new_calendar_setting.field_id.clone(); + let layout_setting_pb: LayoutSettingPB = LayoutSettingParams { + calendar: Some(new_calendar_setting), + } + .into(); + + if let Some(old_calendar_setting) = old_calender_setting { + // compare the new layout field id is equal to old layout field id + // if not equal, send the DidSetNewLayoutField notification + // if equal, send the DidUpdateLayoutSettings notification + if old_calendar_setting.field_id != new_field_id { + send_notification(&self.view_id, DatabaseNotification::DidSetNewLayoutField) + .payload(layout_setting_pb) + .send(); + } else { + send_notification(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings) + .payload(layout_setting_pb) + .send(); + } + } else { + tracing::warn!("Calendar setting should not be empty") + } + } + } + + Ok(()) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn v_did_update_field_type_option( + &self, + field_id: &str, + _old_field: &Field, + ) -> FlowyResult<()> { + if let Some(field) = self.delegate.get_field(field_id).await { + self + .sort_controller + .read() + .await + .did_update_view_field_type_option(&field) + .await; + + // let filter = self + // .delegate + // .get_filter_by_field_id(&self.view_id, field_id); + // + // let old = old_field.map(|old_field| FilterType::from(filter)); + // let new = FilterType::from(field.as_ref()); + // let filter_type = UpdatedFilterType::new(old, new); + // let filter_changeset = FilterChangeset::from_update(filter_type); + // let filter_controller = self.filter_controller.clone(); + // let _ = tokio::spawn(async move { + // if let Some(notification) = filter_controller + // .did_receive_changes(filter_changeset) + // .await + // { + // send_notification(¬ification.view_id, DatabaseNotification::DidUpdateFilter) + // .payload(notification) + // .send(); + // } + // }); + } + Ok(()) + } + + /// + /// + /// # Arguments + /// + /// * `field_id`: + /// + #[tracing::instrument(level = "debug", skip_all, err)] + pub async fn v_update_group_setting(&self, field_id: &str) -> FlowyResult<()> { + if let Some(field) = self.delegate.get_field(field_id).await { + let new_group_controller = + new_group_controller_with_field(self.view_id.clone(), self.delegate.clone(), field).await?; + + let new_groups = new_group_controller + .groups() + .into_iter() + .map(|group| GroupPB::from(group.clone())) + .collect(); + + *self.group_controller.write().await = new_group_controller; + let changeset = GroupChangesetPB { + view_id: self.view_id.clone(), + initial_groups: new_groups, + ..Default::default() + }; + + debug_assert!(!changeset.is_empty()); + if !changeset.is_empty() { + send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField) + .payload(changeset) + .send(); + } + } + Ok(()) + } + + pub async fn v_get_calendar_event(&self, row_id: RowId) -> Option { + let layout_ty = DatabaseLayout::Calendar; + let calendar_setting = self.v_get_layout_settings(&layout_ty).await.calendar?; + + // Text + let primary_field = self.delegate.get_primary_field().await?; + let text_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, row_id).await?; + + // Date + let date_field = self.delegate.get_field(&calendar_setting.field_id).await?; + + let date_cell = get_cell_for_row(self.delegate.clone(), &date_field.id, row_id).await?; + let title = text_cell + .into_text_field_cell_data() + .unwrap_or_default() + .into(); + + let timestamp = date_cell + .into_date_field_cell_data() + .unwrap_or_default() + .timestamp + .unwrap_or_default(); + + Some(CalendarEventPB { + row_id: row_id.into(), + title_field_id: primary_field.id.clone(), + title, + timestamp, + }) + } + + pub async fn v_get_all_calendar_events(&self) -> Option> { + let layout_ty = DatabaseLayout::Calendar; + let calendar_setting = self.v_get_layout_settings(&layout_ty).await.calendar?; + + // Text + let primary_field = self.delegate.get_primary_field().await?; + let text_cells = + get_cells_for_field(self.delegate.clone(), &self.view_id, &primary_field.id).await; + + // Date + let timestamp_by_row_id = get_cells_for_field( + self.delegate.clone(), + &self.view_id, + &calendar_setting.field_id, + ) + .await + .into_iter() + .map(|date_cell| { + let row_id = date_cell.row_id; + + // timestamp + let timestamp = date_cell + .into_date_field_cell_data() + .map(|date_cell_data| date_cell_data.timestamp.unwrap_or_default()) + .unwrap_or_default(); + + (row_id, timestamp) + }) + .collect::>(); + + let mut events: Vec = vec![]; + for text_cell in text_cells { + let title_field_id = text_cell.field_id.clone(); + let row_id = text_cell.row_id; + let timestamp = timestamp_by_row_id + .get(&row_id) + .cloned() + .unwrap_or_default(); + + let title = text_cell + .into_text_field_cell_data() + .unwrap_or_default() + .into(); + + let event = CalendarEventPB { + row_id: row_id.into(), + title_field_id, + title, + timestamp, + }; + events.push(event); + } + Some(events) + } + + pub async fn handle_block_event(&self, event: Cow<'_, DatabaseRowEvent>) { + let changeset = match event.into_owned() { + DatabaseRowEvent::InsertRow(row) => { + RowsChangesetPB::from_insert(self.view_id.clone(), vec![row.into()]) + }, + DatabaseRowEvent::UpdateRow(row) => { + RowsChangesetPB::from_update(self.view_id.clone(), vec![row.into()]) + }, + DatabaseRowEvent::DeleteRow(row_id) => { + RowsChangesetPB::from_delete(self.view_id.clone(), vec![row_id]) + }, + DatabaseRowEvent::Move { + deleted_row_id, + inserted_row, + } => RowsChangesetPB::from_move( + self.view_id.clone(), + vec![deleted_row_id], + vec![inserted_row.into()], + ), + }; + + send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) + .payload(changeset) + .send(); + } + + async fn mut_group_controller(&self, f: F) -> Option + where + F: FnOnce(&mut Box, Arc) -> FlowyResult, + { + let group_field_id = self.group_controller.read().await.field_id().to_owned(); + match self.delegate.get_field(&group_field_id).await { + None => None, + Some(field) => { + let mut write_guard = self.group_controller.write().await; + f(&mut write_guard, field).ok() + }, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs new file mode 100644 index 0000000000..9d64a725b8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -0,0 +1,66 @@ +use crate::services::cell::CellCache; +use crate::services::database_view::{ + gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, +}; +use crate::services::filter::{Filter, FilterController, FilterDelegate, FilterTaskHandler}; +use collab_database::fields::Field; +use collab_database::rows::{Row, RowId}; +use lib_infra::future::{to_fut, Fut}; +use std::sync::Arc; + +pub async fn make_filter_controller( + view_id: &str, + delegate: Arc, + notifier: DatabaseViewChangedNotifier, + cell_cache: CellCache, +) -> Arc { + let filters = delegate.get_all_filters(view_id); + let task_scheduler = delegate.get_task_scheduler(); + let filter_delegate = DatabaseViewFilterDelegateImpl(delegate.clone()); + + let handler_id = gen_handler_id(); + let filter_controller = FilterController::new( + view_id, + &handler_id, + filter_delegate, + task_scheduler.clone(), + filters, + cell_cache, + notifier, + ) + .await; + let filter_controller = Arc::new(filter_controller); + task_scheduler + .write() + .await + .register_handler(FilterTaskHandler::new( + handler_id, + filter_controller.clone(), + )); + filter_controller +} + +struct DatabaseViewFilterDelegateImpl(Arc); + +impl FilterDelegate for DatabaseViewFilterDelegateImpl { + fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>> { + let filter = self.0.get_filter(view_id, filter_id).map(Arc::new); + to_fut(async move { filter }) + } + + fn get_field(&self, field_id: &str) -> Fut>> { + self.0.get_field(field_id) + } + + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { + self.0.get_fields(view_id, field_ids) + } + + fn get_rows(&self, view_id: &str) -> Fut>> { + self.0.get_rows(view_id) + } + + fn get_row(&self, view_id: &str, row_id: RowId) -> Fut)>> { + self.0.get_row(view_id, row_id) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs new file mode 100644 index 0000000000..6feb005d2c --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs @@ -0,0 +1,150 @@ +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::RowId; + +use flowy_error::FlowyResult; +use lib_infra::future::{to_fut, Fut}; + +use crate::entities::FieldType; +use crate::services::database_view::DatabaseViewData; +use crate::services::field::RowSingleCellData; +use crate::services::group::{ + find_new_grouping_field, make_group_controller, GroupController, GroupSetting, + GroupSettingReader, GroupSettingWriter, +}; + +pub async fn new_group_controller_with_field( + view_id: String, + delegate: Arc, + grouping_field: Arc, +) -> FlowyResult> { + let setting_reader = GroupSettingReaderImpl(delegate.clone()); + let rows = delegate.get_rows(&view_id).await; + let setting_writer = GroupSettingWriterImpl(delegate.clone()); + make_group_controller( + view_id, + grouping_field, + rows, + setting_reader, + setting_writer, + ) + .await +} + +pub async fn new_group_controller( + view_id: String, + delegate: Arc, +) -> FlowyResult> { + let setting_reader = GroupSettingReaderImpl(delegate.clone()); + let setting_writer = GroupSettingWriterImpl(delegate.clone()); + + let fields = delegate.get_fields(&view_id, None).await; + let rows = delegate.get_rows(&view_id).await; + let layout = delegate.get_layout_for_view(&view_id); + + // Read the grouping field or find a new grouping field + let grouping_field = setting_reader + .get_group_setting(&view_id) + .await + .and_then(|setting| { + fields + .iter() + .find(|field| field.id == setting.field_id) + .cloned() + }) + .unwrap_or_else(|| find_new_grouping_field(&fields, &layout).unwrap()); + + make_group_controller( + view_id, + grouping_field, + rows, + setting_reader, + setting_writer, + ) + .await +} + +pub(crate) struct GroupSettingReaderImpl(pub Arc); + +impl GroupSettingReader for GroupSettingReaderImpl { + fn get_group_setting(&self, view_id: &str) -> Fut>> { + let mut settings = self.0.get_group_setting(view_id); + to_fut(async move { + if settings.is_empty() { + None + } else { + Some(Arc::new(settings.remove(0))) + } + }) + } + + fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut> { + let field_id = field_id.to_owned(); + let view_id = view_id.to_owned(); + let delegate = self.0.clone(); + to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await }) + } +} + +pub(crate) async fn get_cell_for_row( + delegate: Arc, + field_id: &str, + row_id: RowId, +) -> Option { + let field = delegate.get_field(field_id).await?; + let cell = delegate.get_cell_in_row(field_id, row_id).await?; + let field_type = FieldType::from(field.field_type); + + if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) { + return match handler.get_cell_data(&cell, &field_type, &field) { + Ok(cell_data) => Some(RowSingleCellData { + row_id: cell.row_id, + field_id: field.id.clone(), + field_type: field_type.clone(), + cell_data, + }), + Err(_) => None, + }; + } + None +} + +// Returns the list of cells corresponding to the given field. +pub(crate) async fn get_cells_for_field( + delegate: Arc, + view_id: &str, + field_id: &str, +) -> Vec { + if let Some(field) = delegate.get_field(field_id).await { + let field_type = FieldType::from(field.field_type); + if let Some(handler) = delegate.get_type_option_cell_handler(&field, &field_type) { + let cells = delegate.get_cells_for_field(view_id, field_id).await; + return cells + .iter() + .flat_map( + |cell| match handler.get_cell_data(cell, &field_type, &field) { + Ok(cell_data) => Some(RowSingleCellData { + row_id: cell.row_id, + field_id: field.id.clone(), + field_type: field_type.clone(), + cell_data, + }), + Err(_) => None, + }, + ) + .collect(); + } + } + + vec![] +} + +struct GroupSettingWriterImpl(Arc); + +impl GroupSettingWriter for GroupSettingWriterImpl { + fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut> { + self.0.insert_group_setting(view_id, group_setting); + to_fut(async move { Ok(()) }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs new file mode 100644 index 0000000000..7cac41b3a0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -0,0 +1,77 @@ +use crate::services::cell::CellCache; +use crate::services::database_view::{ + gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, +}; +use crate::services::filter::FilterController; +use crate::services::sort::{Sort, SortController, SortDelegate, SortTaskHandler}; +use collab_database::fields::Field; +use collab_database::rows::Row; +use lib_infra::future::{to_fut, Fut}; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub(crate) async fn make_sort_controller( + view_id: &str, + delegate: Arc, + notifier: DatabaseViewChangedNotifier, + filter_controller: Arc, + cell_cache: CellCache, +) -> Arc> { + let handler_id = gen_handler_id(); + let sorts = delegate + .get_all_sorts(view_id) + .into_iter() + .map(Arc::new) + .collect(); + let task_scheduler = delegate.get_task_scheduler(); + let sort_delegate = DatabaseViewSortDelegateImpl { + delegate, + filter_controller, + }; + let sort_controller = Arc::new(RwLock::new(SortController::new( + view_id, + &handler_id, + sorts, + sort_delegate, + task_scheduler.clone(), + cell_cache, + notifier, + ))); + task_scheduler + .write() + .await + .register_handler(SortTaskHandler::new(handler_id, sort_controller.clone())); + + sort_controller +} + +struct DatabaseViewSortDelegateImpl { + delegate: Arc, + filter_controller: Arc, +} + +impl SortDelegate for DatabaseViewSortDelegateImpl { + fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>> { + let sort = self.delegate.get_sort(view_id, sort_id).map(Arc::new); + to_fut(async move { sort }) + } + + fn get_rows(&self, view_id: &str) -> Fut>> { + let view_id = view_id.to_string(); + let delegate = self.delegate.clone(); + let filter_controller = self.filter_controller.clone(); + to_fut(async move { + let mut rows = delegate.get_rows(&view_id).await; + filter_controller.filter_rows(&mut rows).await; + rows + }) + } + + fn get_field(&self, field_id: &str) -> Fut>> { + self.delegate.get_field(field_id) + } + + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { + self.delegate.get_fields(view_id, field_ids) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs new file mode 100644 index 0000000000..6979a2bc63 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -0,0 +1,151 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::{Row, RowId}; +use nanoid::nanoid; +use tokio::sync::{broadcast, RwLock}; + +use flowy_error::FlowyResult; +use lib_infra::future::Fut; + +use crate::services::cell::CellCache; +use crate::services::database::{DatabaseRowEvent, MutexDatabase}; +use crate::services::database_view::{DatabaseViewData, DatabaseViewEditor}; +use crate::services::group::RowChangeset; + +pub type RowEventSender = broadcast::Sender; +pub type RowEventReceiver = broadcast::Receiver; + +pub struct DatabaseViews { + #[allow(dead_code)] + database: MutexDatabase, + cell_cache: CellCache, + database_view_data: Arc, + editor_map: Arc>>>, +} + +impl DatabaseViews { + pub async fn new( + database: MutexDatabase, + cell_cache: CellCache, + database_view_data: Arc, + row_event_rx: RowEventReceiver, + ) -> FlowyResult { + let editor_map = Arc::new(RwLock::new(HashMap::default())); + listen_on_database_row_event(row_event_rx, editor_map.clone()); + Ok(Self { + database, + database_view_data, + cell_cache, + editor_map, + }) + } + + pub async fn close_view(&self, view_id: &str) -> bool { + let mut editor_map = self.editor_map.write().await; + if let Some(view) = editor_map.remove(view_id) { + view.close().await; + } + editor_map.is_empty() + } + + pub async fn editors(&self) -> Vec> { + self.editor_map.read().await.values().cloned().collect() + } + + /// It may generate a RowChangeset when the Row was moved from one group to another. + /// The return value, [RowChangeset], contains the changes made by the groups. + /// + pub async fn move_group_row( + &self, + view_id: &str, + row: Arc, + to_group_id: String, + to_row_id: Option, + recv_row_changeset: impl FnOnce(RowChangeset) -> Fut<()>, + ) -> FlowyResult<()> { + let view_editor = self.get_view_editor(view_id).await?; + let mut row_changeset = RowChangeset::new(row.id); + view_editor + .v_move_group_row(&row, &mut row_changeset, &to_group_id, to_row_id) + .await; + + if !row_changeset.is_empty() { + recv_row_changeset(row_changeset).await; + } + + Ok(()) + } + + /// Notifies the view's field type-option data is changed + /// For the moment, only the groups will be generated after the type-option data changed. A + /// [Field] has a property named type_options contains a list of type-option data. + /// # Arguments + /// + /// * `field_id`: the id of the field in current view + /// + #[tracing::instrument(level = "debug", skip(self, old_field), err)] + pub async fn did_update_field_type_option( + &self, + view_id: &str, + field_id: &str, + old_field: &Field, + ) -> FlowyResult<()> { + let view_editor = self.get_view_editor(view_id).await?; + // If the id of the grouping field is equal to the updated field's id, then we need to + // update the group setting + if view_editor.group_id().await == field_id { + view_editor.v_update_group_setting(field_id).await?; + } + view_editor + .v_did_update_field_type_option(field_id, old_field) + .await?; + Ok(()) + } + + pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { + debug_assert!(!view_id.is_empty()); + if let Some(editor) = self.editor_map.read().await.get(view_id) { + return Ok(editor.clone()); + } + + tracing::trace!("{:p} create view:{} editor", self, view_id); + let mut editor_map = self.editor_map.write().await; + let editor = Arc::new( + DatabaseViewEditor::new( + view_id.to_owned(), + self.database_view_data.clone(), + self.cell_cache.clone(), + ) + .await?, + ); + editor_map.insert(view_id.to_owned(), editor.clone()); + Ok(editor) + } +} + +fn listen_on_database_row_event( + mut row_event_rx: broadcast::Receiver, + view_editors: Arc>>>, +) { + tokio::spawn(async move { + while let Ok(event) = row_event_rx.recv().await { + let read_guard = view_editors.read().await; + let view_editors = read_guard.values(); + let event = if view_editors.len() == 1 { + Cow::Owned(event) + } else { + Cow::Borrowed(&event) + }; + for view_editor in view_editors { + view_editor.handle_block_event(event.clone()).await; + } + } + }); +} + +pub fn gen_handler_id() -> String { + nanoid!(10) +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs new file mode 100644 index 0000000000..95347214af --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs @@ -0,0 +1,53 @@ +use crate::entities::FieldType; +use crate::services::field::default_type_option_data_from_type; +use collab_database::database::gen_field_id; +use collab_database::fields::{Field, TypeOptionData}; + +pub struct FieldBuilder { + field: Field, +} + +impl FieldBuilder { + pub fn new>(field_type: FieldType, type_option_data: T) -> Self { + let mut field = Field::new( + gen_field_id(), + "".to_string(), + field_type.clone().into(), + false, + ); + field.width = field_type.default_cell_width() as i64; + field + .type_options + .insert(field_type.to_string(), type_option_data.into()); + Self { field } + } + + pub fn from_field_type(field_type: FieldType) -> Self { + let type_option_data = default_type_option_data_from_type(&field_type); + Self::new(field_type, type_option_data) + } + + pub fn name(mut self, name: &str) -> Self { + self.field.name = name.to_owned(); + self + } + + pub fn primary(mut self, is_primary: bool) -> Self { + self.field.is_primary = is_primary; + self + } + + pub fn visibility(mut self, visibility: bool) -> Self { + self.field.visibility = visibility; + self + } + + pub fn width(mut self, width: i64) -> Self { + self.field.width = width; + self + } + + pub fn build(self) -> Field { + self.field + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs b/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs new file mode 100644 index 0000000000..1a5715163a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use collab_database::fields::TypeOptionData; + +use flowy_error::FlowyResult; + +use crate::entities::FieldType; +use crate::services::database::DatabaseEditor; +use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; + +pub async fn edit_field_type_option + Into>( + view_id: &str, + field_id: &str, + editor: Arc, + action: impl FnOnce(&mut T), +) -> FlowyResult<()> { + let get_type_option = async { + let field = editor.get_field(field_id)?; + let field_type = FieldType::from(field.field_type); + field.get_type_option::(field_type) + }; + + if let Some(mut type_option) = get_type_option.await { + if let Some(old_field) = editor.get_field(field_id) { + action(&mut type_option); + let type_option_data: TypeOptionData = type_option.into(); + editor + .update_field_type_option(view_id, field_id, type_option_data, old_field) + .await?; + } + } + + Ok(()) +} + +pub async fn edit_single_select_type_option( + view_id: &str, + field_id: &str, + editor: Arc, + action: impl FnOnce(&mut SingleSelectTypeOption), +) -> FlowyResult<()> { + edit_field_type_option(view_id, field_id, editor, action).await +} + +pub async fn edit_multi_select_type_option( + view_id: &str, + field_id: &str, + editor: Arc, + action: impl FnOnce(&mut MultiSelectTypeOption), +) -> FlowyResult<()> { + edit_field_type_option(view_id, field_id, editor, action).await +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/mod.rs new file mode 100644 index 0000000000..8f06361e2f --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/mod.rs @@ -0,0 +1,9 @@ +mod field_builder; +mod field_operation; +mod type_option_builder; +mod type_options; + +pub use field_builder::*; +pub use field_operation::*; +pub use type_option_builder::*; +pub use type_options::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_option_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_option_builder.rs new file mode 100644 index 0000000000..e69f664bf6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_option_builder.rs @@ -0,0 +1,16 @@ +use crate::entities::FieldType; +use crate::services::field::type_options::*; +use collab_database::fields::TypeOptionData; + +pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionData { + match field_type { + FieldType::RichText => RichTextTypeOption::default().into(), + FieldType::Number => NumberTypeOption::default().into(), + FieldType::DateTime => DateTypeOption::default().into(), + FieldType::SingleSelect => SingleSelectTypeOption::default().into(), + FieldType::MultiSelect => MultiSelectTypeOption::default().into(), + FieldType::Checkbox => CheckboxTypeOption::default().into(), + FieldType::URL => URLTypeOption::default().into(), + FieldType::Checklist => ChecklistTypeOption::default().into(), + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs new file mode 100644 index 0000000000..4dbb991e3e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs @@ -0,0 +1,51 @@ +use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB}; +use crate::services::field::CheckboxCellData; + +impl CheckboxFilterPB { + pub fn is_visible(&self, cell_data: &CheckboxCellData) -> bool { + let is_check = cell_data.is_check(); + match self.condition { + CheckboxFilterConditionPB::IsChecked => is_check, + CheckboxFilterConditionPB::IsUnChecked => !is_check, + } + } +} + +#[cfg(test)] +mod tests { + use crate::entities::{CheckboxFilterConditionPB, CheckboxFilterPB}; + use crate::services::field::CheckboxCellData; + use std::str::FromStr; + + #[test] + fn checkbox_filter_is_check_test() { + let checkbox_filter = CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + }; + for (value, visible) in [ + ("true", true), + ("yes", true), + ("false", false), + ("no", false), + ] { + let data = CheckboxCellData::from_str(value).unwrap(); + assert_eq!(checkbox_filter.is_visible(&data), visible); + } + } + + #[test] + fn checkbox_filter_is_uncheck_test() { + let checkbox_filter = CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsUnChecked, + }; + for (value, visible) in [ + ("false", true), + ("no", true), + ("true", false), + ("yes", false), + ] { + let data = CheckboxCellData::from_str(value).unwrap(); + assert_eq!(checkbox_filter.is_visible(&data), visible); + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs new file mode 100644 index 0000000000..1d17755fc2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs @@ -0,0 +1,54 @@ +#[cfg(test)] +mod tests { + use collab_database::fields::Field; + + use crate::entities::FieldType; + use crate::services::cell::CellDataDecoder; + use crate::services::cell::FromCellString; + use crate::services::field::type_options::checkbox_type_option::*; + use crate::services::field::FieldBuilder; + + #[test] + fn checkout_box_description_test() { + let type_option = CheckboxTypeOption::default(); + let field_type = FieldType::Checkbox; + let field_rev = FieldBuilder::from_field_type(field_type.clone()).build(); + + // the checkout value will be checked if the value is "1", "true" or "yes" + assert_checkbox(&type_option, "1", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "true", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "TRUE", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "yes", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "YES", CHECK, &field_type, &field_rev); + + // the checkout value will be uncheck if the value is "false" or "No" + assert_checkbox(&type_option, "false", UNCHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "No", UNCHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "NO", UNCHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "0", UNCHECK, &field_type, &field_rev); + + // the checkout value will be empty if the value is letters or empty string + assert_checkbox(&type_option, "abc", "", &field_type, &field_rev); + assert_checkbox(&type_option, "", "", &field_type, &field_rev); + } + + fn assert_checkbox( + type_option: &CheckboxTypeOption, + input_str: &str, + expected_str: &str, + field_type: &FieldType, + field: &Field, + ) { + assert_eq!( + type_option + .decode_cell_str( + &CheckboxCellData::from_cell_str(input_str).unwrap().into(), + field_type, + field + ) + .unwrap() + .to_string(), + expected_str.to_owned() + ); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs new file mode 100644 index 0000000000..1922da36a2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -0,0 +1,145 @@ +use crate::entities::{CheckboxFilterPB, FieldType}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::{ + default_order, CheckboxCellData, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionTransform, +}; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::str::FromStr; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CheckboxTypeOption { + pub is_selected: bool, +} + +impl TypeOption for CheckboxTypeOption { + type CellData = CheckboxCellData; + type CellChangeset = CheckboxCellChangeset; + type CellProtobufType = CheckboxCellData; + type CellFilter = CheckboxFilterPB; +} + +impl TypeOptionTransform for CheckboxTypeOption { + fn transformable(&self) -> bool { + true + } + + fn transform_type_option( + &mut self, + _old_type_option_field_type: FieldType, + _old_type_option_data: TypeOptionData, + ) { + } + + fn transform_type_option_cell( + &self, + cell: &Cell, + _decoded_field_type: &FieldType, + _field: &Field, + ) -> Option<::CellData> { + if _decoded_field_type.is_text() { + Some(CheckboxCellData::from(cell)) + } else { + None + } + } +} + +impl From for CheckboxTypeOption { + fn from(data: TypeOptionData) -> Self { + let is_selected = data.get_bool_value("is_selected").unwrap_or(false); + CheckboxTypeOption { is_selected } + } +} + +impl From for TypeOptionData { + fn from(data: CheckboxTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_bool_value("is_selected", data.is_selected) + .build() + } +} + +impl TypeOptionCellData for CheckboxTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + cell_data + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(CheckboxCellData::from(cell)) + } +} + +impl CellDataDecoder for CheckboxTypeOption { + fn decode_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + if !decoded_field_type.is_checkbox() { + return Ok(Default::default()); + } + + self.decode_cell(cell) + } + + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String { + cell_data.to_string() + } + + fn decode_cell_to_str(&self, cell: &Cell) -> String { + Self::CellData::from(cell).to_string() + } +} + +pub type CheckboxCellChangeset = String; + +impl CellDataChangeset for CheckboxTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let checkbox_cell_data = CheckboxCellData::from_str(&changeset)?; + Ok((checkbox_cell_data.clone().into(), checkbox_cell_data)) + } +} + +impl TypeOptionCellDataFilter for CheckboxTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_checkbox() { + return true; + } + filter.is_visible(cell_data) + } +} + +impl TypeOptionCellDataCompare for CheckboxTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + match (cell_data.is_check(), other_cell_data.is_check()) { + (true, true) => Ordering::Equal, + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (false, false) => default_order(), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs new file mode 100644 index 0000000000..a81001c09d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -0,0 +1,115 @@ +use crate::entities::FieldType; +use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString}; +use crate::services::field::CELL_DATE; +use bytes::Bytes; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use flowy_error::{FlowyError, FlowyResult}; +use protobuf::ProtobufError; +use std::str::FromStr; + +pub const CHECK: &str = "Yes"; +pub const UNCHECK: &str = "No"; + +#[derive(Default, Debug, Clone)] +pub struct CheckboxCellData(pub String); + +impl CheckboxCellData { + pub fn into_inner(self) -> bool { + self.is_check() + } + + pub fn is_check(&self) -> bool { + self.0 == CHECK + } + + pub fn is_uncheck(&self) -> bool { + self.0 == UNCHECK + } +} + +impl AsRef<[u8]> for CheckboxCellData { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From<&Cell> for CheckboxCellData { + fn from(cell: &Cell) -> Self { + let value = cell.get_str_value(CELL_DATE).unwrap_or_default(); + CheckboxCellData::from_cell_str(&value).unwrap_or_default() + } +} + +impl From for Cell { + fn from(data: CheckboxCellData) -> Self { + new_cell_builder(FieldType::Checkbox) + .insert_str_value(CELL_DATE, data.to_string()) + .build() + } +} + +impl FromStr for CheckboxCellData { + type Err = FlowyError; + + fn from_str(s: &str) -> Result { + let lower_case_str: &str = &s.to_lowercase(); + let val = match lower_case_str { + "1" => Some(true), + "true" => Some(true), + "yes" => Some(true), + "0" => Some(false), + "false" => Some(false), + "no" => Some(false), + _ => None, + }; + + match val { + Some(true) => Ok(Self(CHECK.to_string())), + Some(false) => Ok(Self(UNCHECK.to_string())), + None => Ok(Self("".to_string())), + } + } +} + +impl std::convert::TryFrom for Bytes { + type Error = ProtobufError; + + fn try_from(value: CheckboxCellData) -> Result { + Ok(Bytes::from(value.0)) + } +} + +impl FromCellString for CheckboxCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Self::from_str(s) + } +} + +impl ToString for CheckboxCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl DecodedCellData for CheckboxCellData { + type Object = CheckboxCellData; + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +pub struct CheckboxCellDataParser(); +impl CellProtobufBlobParser for CheckboxCellDataParser { + type Object = CheckboxCellData; + fn parser(bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => CheckboxCellData::from_cell_str(&s), + Err(_) => Ok(CheckboxCellData("".to_string())), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/mod.rs new file mode 100644 index 0000000000..309072caa6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/mod.rs @@ -0,0 +1,8 @@ +#![allow(clippy::module_inception)] +mod checkbox_filter; +mod checkbox_tests; +mod checkbox_type_option; +mod checkbox_type_option_entities; + +pub use checkbox_type_option::*; +pub use checkbox_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs new file mode 100644 index 0000000000..35f30b388e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs @@ -0,0 +1,149 @@ +use crate::entities::{DateFilterConditionPB, DateFilterPB}; +use chrono::NaiveDateTime; + +impl DateFilterPB { + pub fn is_visible>>(&self, cell_timestamp: T) -> bool { + match cell_timestamp.into() { + None => DateFilterConditionPB::DateIsEmpty == self.condition, + Some(timestamp) => { + match self.condition { + DateFilterConditionPB::DateIsNotEmpty => { + return true; + }, + DateFilterConditionPB::DateIsEmpty => { + return false; + }, + _ => {}, + } + + let cell_time = NaiveDateTime::from_timestamp_opt(timestamp, 0); + let cell_date = cell_time.map(|time| time.date()); + match self.timestamp { + None => { + if self.start.is_none() { + return true; + } + + if self.end.is_none() { + return true; + } + + let start_time = NaiveDateTime::from_timestamp_opt(*self.start.as_ref().unwrap(), 0); + let start_date = start_time.map(|time| time.date()); + + let end_time = NaiveDateTime::from_timestamp_opt(*self.end.as_ref().unwrap(), 0); + let end_date = end_time.map(|time| time.date()); + + cell_date >= start_date && cell_date <= end_date + }, + Some(timestamp) => { + let expected_timestamp = NaiveDateTime::from_timestamp_opt(timestamp, 0); + let expected_date = expected_timestamp.map(|time| time.date()); + + // We assume that the cell_timestamp doesn't contain hours, just day. + match self.condition { + DateFilterConditionPB::DateIs => cell_date == expected_date, + DateFilterConditionPB::DateBefore => cell_date < expected_date, + DateFilterConditionPB::DateAfter => cell_date > expected_date, + DateFilterConditionPB::DateOnOrBefore => cell_date <= expected_date, + DateFilterConditionPB::DateOnOrAfter => cell_date >= expected_date, + _ => true, + } + }, + } + }, + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::all)] + use crate::entities::{DateFilterConditionPB, DateFilterPB}; + + #[test] + fn date_filter_is_test() { + let filter = DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: Some(1668387885), + end: None, + start: None, + }; + + for (val, visible) in vec![(1668387885, true), (1647251762, false)] { + assert_eq!(filter.is_visible(val as i64), visible); + } + } + #[test] + fn date_filter_before_test() { + let filter = DateFilterPB { + condition: DateFilterConditionPB::DateBefore, + timestamp: Some(1668387885), + start: None, + end: None, + }; + + for (val, visible, msg) in vec![(1668387884, false, "1"), (1647251762, true, "2")] { + assert_eq!(filter.is_visible(val as i64), visible, "{}", msg); + } + } + + #[test] + fn date_filter_before_or_on_test() { + let filter = DateFilterPB { + condition: DateFilterConditionPB::DateOnOrBefore, + timestamp: Some(1668387885), + start: None, + end: None, + }; + + for (val, visible) in vec![(1668387884, true), (1668387885, true)] { + assert_eq!(filter.is_visible(val as i64), visible); + } + } + #[test] + fn date_filter_after_test() { + let filter = DateFilterPB { + condition: DateFilterConditionPB::DateAfter, + timestamp: Some(1668387885), + start: None, + end: None, + }; + + for (val, visible) in vec![(1668387888, false), (1668531885, true), (0, false)] { + assert_eq!(filter.is_visible(val as i64), visible); + } + } + + #[test] + fn date_filter_within_test() { + let filter = DateFilterPB { + condition: DateFilterConditionPB::DateWithIn, + start: Some(1668272685), // 11/13 + end: Some(1668618285), // 11/17 + timestamp: None, + }; + + for (val, visible, _msg) in vec![ + (1668272685, true, "11/13"), + (1668359085, true, "11/14"), + (1668704685, false, "11/18"), + ] { + assert_eq!(filter.is_visible(val as i64), visible); + } + } + + #[test] + fn date_filter_is_empty_test() { + let filter = DateFilterPB { + condition: DateFilterConditionPB::DateIsEmpty, + start: None, + end: None, + timestamp: None, + }; + + for (val, visible) in vec![(None, true), (Some(123), false)] { + assert_eq!(filter.is_visible(val), visible); + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs new file mode 100644 index 0000000000..67886475b9 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs @@ -0,0 +1,261 @@ +#[cfg(test)] +mod tests { + use chrono::format::strftime::StrftimeItems; + use chrono::{FixedOffset, NaiveDateTime}; + use collab_database::fields::Field; + use collab_database::rows::Cell; + use strum::IntoEnumIterator; + + use crate::entities::FieldType; + use crate::services::cell::{CellDataChangeset, CellDataDecoder}; + use crate::services::field::{ + DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat, TypeOptionCellData, + }; + + #[test] + fn date_type_option_date_format_test() { + let mut type_option = DateTypeOption::default(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + for date_format in DateFormat::iter() { + type_option.date_format = date_format; + match date_format { + DateFormat::Friendly => { + assert_date(&type_option, 1647251762, None, "Mar 14,2022", false, &field); + }, + DateFormat::US => { + assert_date(&type_option, 1647251762, None, "2022/03/14", false, &field); + }, + DateFormat::ISO => { + assert_date(&type_option, 1647251762, None, "2022-03-14", false, &field); + }, + DateFormat::Local => { + assert_date(&type_option, 1647251762, None, "03/14/2022", false, &field); + }, + DateFormat::DayMonthYear => { + assert_date(&type_option, 1647251762, None, "14/03/2022", false, &field); + }, + } + } + } + + #[test] + fn date_type_option_different_time_format_test() { + let mut type_option = DateTypeOption::default(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(field_type).build(); + + for time_format in TimeFormat::iter() { + type_option.time_format = time_format; + match time_format { + TimeFormat::TwentyFourHour => { + assert_date( + &type_option, + 1653609600, + None, + "May 27,2022 00:00", + true, + &field_rev, + ); + assert_date( + &type_option, + 1653609600, + Some("9:00".to_owned()), + "May 27,2022 09:00", + true, + &field_rev, + ); + assert_date( + &type_option, + 1653609600, + Some("23:00".to_owned()), + "May 27,2022 23:00", + true, + &field_rev, + ); + }, + TimeFormat::TwelveHour => { + assert_date( + &type_option, + 1653609600, + None, + "May 27,2022 12:00 AM", + true, + &field_rev, + ); + assert_date( + &type_option, + 1653609600, + Some("9:00 AM".to_owned()), + "May 27,2022 09:00 AM", + true, + &field_rev, + ); + assert_date( + &type_option, + 1653609600, + Some("11:23 pm".to_owned()), + "May 27,2022 11:23 PM", + true, + &field_rev, + ); + }, + } + } + } + + #[test] + fn date_type_option_invalid_date_str_test() { + let type_option = DateTypeOption::default(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(field_type).build(); + assert_date(&type_option, "abc", None, "", false, &field_rev); + } + + #[test] + #[should_panic] + fn date_type_option_invalid_include_time_str_test() { + let type_option = DateTypeOption::new(); + let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("1:".to_owned()), + "May 27,2022 01:00", + true, + &field_rev, + ); + } + + #[test] + fn date_type_option_empty_include_time_str_test() { + let type_option = DateTypeOption::new(); + let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("".to_owned()), + "May 27,2022 00:00", + true, + &field_rev, + ); + } + + #[test] + fn date_type_midnight_include_time_str_test() { + let type_option = DateTypeOption::new(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(field_type).build(); + assert_date( + &type_option, + 1653609600, + Some("00:00".to_owned()), + "May 27,2022 00:00", + true, + &field_rev, + ); + } + + /// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error. + #[test] + #[should_panic] + fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { + let type_option = DateTypeOption::new(); + let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("1:00 am".to_owned()), + "May 27,2022 01:00 AM", + true, + &field_rev, + ); + } + + // Attempting to parse include_time_str as TwelveHour when TwentyFourHour format is given should cause parser error. + #[test] + #[should_panic] + fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() { + let mut type_option = DateTypeOption::new(); + type_option.time_format = TimeFormat::TwelveHour; + let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("20:00".to_owned()), + "May 27,2022 08:00 PM", + true, + &field_rev, + ); + } + + #[test] + fn utc_to_native_test() { + let native_timestamp = 1647251762; + let native = NaiveDateTime::from_timestamp_opt(native_timestamp, 0).unwrap(); + + let utc = chrono::DateTime::::from_utc(native, chrono::Utc); + // utc_timestamp doesn't carry timezone + let utc_timestamp = utc.timestamp(); + assert_eq!(native_timestamp, utc_timestamp); + + let format = "%m/%d/%Y %I:%M %p".to_string(); + let native_time_str = format!("{}", native.format_with_items(StrftimeItems::new(&format))); + let utc_time_str = format!("{}", utc.format_with_items(StrftimeItems::new(&format))); + assert_eq!(native_time_str, utc_time_str); + + // Mon Mar 14 2022 17:56:02 GMT+0800 (China Standard Time) + let gmt_8_offset = FixedOffset::east_opt(8 * 3600).unwrap(); + let china_local = chrono::DateTime::::from_utc(native, gmt_8_offset); + let china_local_time = format!( + "{}", + china_local.format_with_items(StrftimeItems::new(&format)) + ); + + assert_eq!(china_local_time, "03/14/2022 05:56 PM"); + } + + fn assert_date( + type_option: &DateTypeOption, + timestamp: T, + include_time_str: Option, + expected_str: &str, + include_time: bool, + field: &Field, + ) { + let changeset = DateCellChangeset { + date: Some(timestamp.to_string()), + time: include_time_str, + is_utc: false, + include_time: Some(include_time), + }; + let (cell, _) = type_option.apply_changeset(changeset, None).unwrap(); + + assert_eq!( + decode_cell_data(&cell, type_option, include_time, field), + expected_str.to_owned(), + ); + } + + fn decode_cell_data( + cell: &Cell, + type_option: &DateTypeOption, + include_time: bool, + field: &Field, + ) -> String { + let decoded_data = type_option + .decode_cell_str(cell, &FieldType::DateTime, field) + .unwrap(); + let decoded_data = type_option.convert_to_protobuf(decoded_data); + if include_time { + format!("{} {}", decoded_data.date, decoded_data.time) + .trim_end() + .to_owned() + } else { + decoded_data.date + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs new file mode 100644 index 0000000000..0dd3dd471b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -0,0 +1,234 @@ +use crate::entities::{DateCellDataPB, DateFilterPB, FieldType}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::{ + default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption, + TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, +}; +use chrono::format::strftime::StrftimeItems; +use chrono::NaiveDateTime; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +// Date +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct DateTypeOption { + pub date_format: DateFormat, + pub time_format: TimeFormat, + pub include_time: bool, +} + +impl TypeOption for DateTypeOption { + type CellData = DateCellData; + type CellChangeset = DateCellChangeset; + type CellProtobufType = DateCellDataPB; + type CellFilter = DateFilterPB; +} + +impl From for DateTypeOption { + fn from(data: TypeOptionData) -> Self { + let include_time = data.get_bool_value("include_time").unwrap_or(false); + let date_format = data + .get_i64_value("data_format") + .map(DateFormat::from) + .unwrap_or_default(); + let time_format = data + .get_i64_value("time_format") + .map(TimeFormat::from) + .unwrap_or_default(); + Self { + date_format, + time_format, + include_time, + } + } +} + +impl From for TypeOptionData { + fn from(data: DateTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_i64_value("data_format", data.date_format.value()) + .insert_i64_value("time_format", data.time_format.value()) + .insert_bool_value("include_time", data.include_time) + .build() + } +} + +impl TypeOptionCellData for DateTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + self.today_desc_from_timestamp(cell_data) + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(DateCellData::from(cell)) + } +} + +impl DateTypeOption { + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } + + fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB { + let timestamp = cell_data.timestamp.unwrap_or_default(); + let include_time = cell_data.include_time; + + let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0); + if naive.is_none() { + return DateCellDataPB::default(); + } + let naive = naive.unwrap(); + if timestamp == 0 { + return DateCellDataPB::default(); + } + let fmt = self.date_format.format_str(); + let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt))); + + let time = if include_time { + let fmt = self.time_format.format_str(); + format!("{}", naive.format_with_items(StrftimeItems::new(fmt))) + } else { + "".to_string() + }; + + DateCellDataPB { + date, + time, + include_time, + timestamp, + } + } + + fn timestamp_from_utc_with_time( + &self, + naive_date: &NaiveDateTime, + time_str: &Option, + ) -> FlowyResult { + if let Some(time_str) = time_str.as_ref() { + if !time_str.is_empty() { + let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str()); + + match naive_time { + Ok(naive_time) => { + return Ok(naive_date.date().and_time(naive_time).timestamp()); + }, + Err(_e) => { + let msg = format!("Parse {} failed", time_str); + return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)); + }, + }; + } + } + + Ok(naive_date.timestamp()) + } +} + +impl TypeOptionTransform for DateTypeOption {} + +impl CellDataDecoder for DateTypeOption { + fn decode_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + // Return default data if the type_option_cell_data is not FieldType::DateTime. + // It happens when switching from one field to another. + // For example: + // FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen. + if !decoded_field_type.is_date() { + return Ok(Default::default()); + } + + self.decode_cell(cell) + } + + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String { + self.today_desc_from_timestamp(cell_data).date + } + + fn decode_cell_to_str(&self, cell: &Cell) -> String { + let cell_data = Self::CellData::from(cell); + self.decode_cell_data_to_str(cell_data) + } +} + +impl CellDataChangeset for DateTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let (timestamp, include_time) = match cell { + None => (None, false), + Some(cell) => { + let cell_data = DateCellData::from(&cell); + (cell_data.timestamp, cell_data.include_time) + }, + }; + + let include_time = match changeset.include_time { + None => include_time, + Some(include_time) => include_time, + }; + let timestamp = match changeset.date_timestamp() { + None => timestamp, + Some(date_timestamp) => match (include_time, changeset.time) { + (true, Some(time)) => { + let time = Some(time.trim().to_uppercase()); + let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0); + if let Some(naive) = naive { + Some(self.timestamp_from_utc_with_time(&naive, &time)?) + } else { + Some(date_timestamp) + } + }, + _ => Some(date_timestamp), + }, + }; + + let date_cell_data = DateCellData { + timestamp, + include_time, + }; + Ok((date_cell_data.clone().into(), date_cell_data)) + } +} + +impl TypeOptionCellDataFilter for DateTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_date() { + return true; + } + + filter.is_visible(cell_data.timestamp) + } +} + +impl TypeOptionCellDataCompare for DateTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + match (cell_data.timestamp, other_cell_data.timestamp) { + (Some(left), Some(right)) => left.cmp(&right), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => default_order(), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs new file mode 100644 index 0000000000..5b7a9da9c5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -0,0 +1,266 @@ +#![allow(clippy::upper_case_acronyms)] + +use std::fmt; + +use bytes::Bytes; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use serde::de::Visitor; +use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; + +use flowy_error::{internal_error, FlowyResult}; + +use crate::entities::{DateCellDataPB, FieldType}; +use crate::services::cell::{ + CellProtobufBlobParser, DecodedCellData, FromCellChangeset, FromCellString, ToCellChangeset, +}; +use crate::services::field::CELL_DATE; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DateCellChangeset { + pub date: Option, + pub time: Option, + pub include_time: Option, + pub is_utc: bool, +} + +impl DateCellChangeset { + pub fn date_timestamp(&self) -> Option { + if let Some(date) = &self.date { + match date.parse::() { + Ok(date_timestamp) => Some(date_timestamp), + Err(_) => None, + } + } else { + None + } + } +} + +impl FromCellChangeset for DateCellChangeset { + fn from_changeset(changeset: String) -> FlowyResult + where + Self: Sized, + { + serde_json::from_str::(&changeset).map_err(internal_error) + } +} + +impl ToCellChangeset for DateCellChangeset { + fn to_cell_changeset_str(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } +} + +#[derive(Default, Clone, Debug, Serialize)] +pub struct DateCellData { + pub timestamp: Option, + pub include_time: bool, +} + +impl From<&Cell> for DateCellData { + fn from(cell: &Cell) -> Self { + let timestamp = cell + .get_str_value(CELL_DATE) + .map(|data| data.parse::().unwrap_or_default()); + + let include_time = cell.get_bool_value("include_time").unwrap_or_default(); + Self { + timestamp, + include_time, + } + } +} + +impl From for Cell { + fn from(data: DateCellData) -> Self { + new_cell_builder(FieldType::DateTime) + .insert_str_value(CELL_DATE, data.timestamp.unwrap_or_default().to_string()) + .insert_bool_value("include_time", data.include_time) + .build() + } +} + +impl<'de> serde::Deserialize<'de> for DateCellData { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + struct DateCellVisitor(); + + impl<'de> Visitor<'de> for DateCellVisitor { + type Value = DateCellData; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "DateCellData with type: str containing either an integer timestamp or the JSON representation", + ) + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + Ok(DateCellData { + timestamp: Some(value), + include_time: false, + }) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + self.visit_i64(value as i64) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut timestamp: Option = None; + let mut include_time: Option = None; + + while let Some(key) = map.next_key()? { + match key { + "timestamp" => { + timestamp = map.next_value()?; + }, + "include_time" => { + include_time = map.next_value()?; + }, + _ => {}, + } + } + + let include_time = include_time.unwrap_or(false); + + Ok(DateCellData { + timestamp, + include_time, + }) + } + } + + deserializer.deserialize_any(DateCellVisitor()) + } +} + +impl FromCellString for DateCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + let result: DateCellData = serde_json::from_str(s).unwrap(); + Ok(result) + } +} + +impl ToString for DateCellData { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize)] +pub enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, + DayMonthYear = 4, +} +impl std::default::Default for DateFormat { + fn default() -> Self { + DateFormat::Friendly + } +} + +impl std::convert::From for DateFormat { + fn from(value: i64) -> Self { + match value { + 0 => DateFormat::Local, + 1 => DateFormat::US, + 2 => DateFormat::ISO, + 3 => DateFormat::Friendly, + 4 => DateFormat::DayMonthYear, + _ => { + tracing::error!("Unsupported date format, fallback to friendly"); + DateFormat::Friendly + }, + } + } +} + +impl DateFormat { + pub fn value(&self) -> i64 { + *self as i64 + } + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + DateFormat::Local => "%m/%d/%Y", + DateFormat::US => "%Y/%m/%d", + DateFormat::ISO => "%Y-%m-%d", + DateFormat::Friendly => "%b %d,%Y", + DateFormat::DayMonthYear => "%d/%m/%Y", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize)] +pub enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +impl std::convert::From for TimeFormat { + fn from(value: i64) -> Self { + match value { + 0 => TimeFormat::TwelveHour, + 1 => TimeFormat::TwentyFourHour, + _ => { + tracing::error!("Unsupported time format, fallback to TwentyFourHour"); + TimeFormat::TwentyFourHour + }, + } + } +} + +impl TimeFormat { + pub fn value(&self) -> i64 { + *self as i64 + } + + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + TimeFormat::TwelveHour => "%I:%M %p", + TimeFormat::TwentyFourHour => "%R", + } + } +} + +impl std::default::Default for TimeFormat { + fn default() -> Self { + TimeFormat::TwentyFourHour + } +} + +impl DecodedCellData for DateCellDataPB { + type Object = DateCellDataPB; + + fn is_empty(&self) -> bool { + self.date.is_empty() + } +} + +pub struct DateCellDataParser(); +impl CellProtobufBlobParser for DateCellDataParser { + type Object = DateCellDataPB; + + fn parser(bytes: &Bytes) -> FlowyResult { + DateCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs new file mode 100644 index 0000000000..ff0c344957 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs @@ -0,0 +1,8 @@ +#![allow(clippy::module_inception)] +mod date_filter; +mod date_tests; +mod date_type_option; +mod date_type_option_entities; + +pub use date_type_option::*; +pub use date_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs new file mode 100644 index 0000000000..0e909d2cf2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs @@ -0,0 +1,18 @@ +pub mod checkbox_type_option; +pub mod date_type_option; +pub mod number_type_option; +pub mod selection_type_option; +pub mod text_type_option; +mod type_option; +mod type_option_cell; +mod url_type_option; +mod util; + +pub use checkbox_type_option::*; +pub use date_type_option::*; +pub use number_type_option::*; +pub use selection_type_option::*; +pub use text_type_option::*; +pub use type_option::*; +pub use type_option_cell::*; +pub use url_type_option::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/format.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/format.rs new file mode 100644 index 0000000000..6c30180cbb --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/format.rs @@ -0,0 +1,504 @@ +#![allow(clippy::upper_case_acronyms)] + +use lazy_static::lazy_static; +use rusty_money::define_currency_set; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +lazy_static! { + pub static ref CURRENCY_SYMBOL: Vec = NumberFormat::iter() + .map(|format| format.symbol()) + .collect::>(); + pub static ref STRIP_SYMBOL: Vec = vec![",".to_owned(), ".".to_owned()]; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize)] +pub enum NumberFormat { + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +impl NumberFormat { + pub fn value(&self) -> i64 { + *self as i64 + } +} + +impl std::default::Default for NumberFormat { + fn default() -> Self { + NumberFormat::Num + } +} + +impl From for NumberFormat { + fn from(value: i64) -> Self { + match value { + 0 => NumberFormat::Num, + 1 => NumberFormat::USD, + 2 => NumberFormat::CanadianDollar, + 4 => NumberFormat::EUR, + 5 => NumberFormat::Pound, + 6 => NumberFormat::Yen, + 7 => NumberFormat::Ruble, + 8 => NumberFormat::Rupee, + 9 => NumberFormat::Won, + 10 => NumberFormat::Yuan, + 11 => NumberFormat::Real, + 12 => NumberFormat::Lira, + 13 => NumberFormat::Rupiah, + 14 => NumberFormat::Franc, + 15 => NumberFormat::HongKongDollar, + 16 => NumberFormat::NewZealandDollar, + 17 => NumberFormat::Krona, + 18 => NumberFormat::NorwegianKrone, + 19 => NumberFormat::MexicanPeso, + 20 => NumberFormat::Rand, + 21 => NumberFormat::NewTaiwanDollar, + 22 => NumberFormat::DanishKrone, + 23 => NumberFormat::Baht, + 24 => NumberFormat::Forint, + 25 => NumberFormat::Koruna, + 26 => NumberFormat::Shekel, + 27 => NumberFormat::ChileanPeso, + 28 => NumberFormat::PhilippinePeso, + 29 => NumberFormat::Dirham, + 30 => NumberFormat::ColombianPeso, + 31 => NumberFormat::Riyal, + 32 => NumberFormat::Ringgit, + 33 => NumberFormat::Leu, + 34 => NumberFormat::ArgentinePeso, + 35 => NumberFormat::UruguayanPeso, + 36 => NumberFormat::Percent, + _ => NumberFormat::Num, + } + } +} + +define_currency_set!( + number_currency { + NUMBER : { + code: "", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "number", + symbol: "RUB", + symbol_first: false, + }, + PERCENT : { + code: "", + exponent: 2, + locale: EnIn, + minor_units: 1, + name: "percent", + symbol: "%", + symbol_first: false, + }, + USD : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "United States Dollar", + symbol: "$", + symbol_first: true, + }, + CANADIAN_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Canadian Dollar", + symbol: "CA$", + symbol_first: true, + }, + NEW_TAIWAN_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "NewTaiwan Dollar", + symbol: "NT$", + symbol_first: true, + }, + HONG_KONG_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "HongKong Dollar", + symbol: "HZ$", + symbol_first: true, + }, + NEW_ZEALAND_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "NewZealand Dollar", + symbol: "NZ$", + symbol_first: true, + }, + EUR : { + code: "EUR", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Euro", + symbol: "€", + symbol_first: true, + }, + GIP : { + code: "GIP", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Gibraltar Pound", + symbol: "£", + symbol_first: true, + }, + CNY : { + code: "CNY", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Chinese Renminbi Yuan", + symbol: "¥", + symbol_first: true, + }, + YUAN : { + code: "CNY", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Chinese Renminbi Yuan", + symbol: "CN¥", + symbol_first: true, + }, + RUB : { + code: "RUB", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Russian Ruble", + symbol: "RUB", + symbol_first: false, + }, + INR : { + code: "INR", + exponent: 2, + locale: EnIn, + minor_units: 50, + name: "Indian Rupee", + symbol: "₹", + symbol_first: true, + }, + KRW : { + code: "KRW", + exponent: 0, + locale: EnUs, + minor_units: 1, + name: "South Korean Won", + symbol: "₩", + symbol_first: true, + }, + BRL : { + code: "BRL", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Brazilian real", + symbol: "R$", + symbol_first: true, + }, + TRY : { + code: "TRY", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Turkish Lira", + // symbol: "₺", + symbol: "TRY", + symbol_first: true, + }, + IDR : { + code: "IDR", + exponent: 2, + locale: EnUs, + minor_units: 5000, + name: "Indonesian Rupiah", + // symbol: "Rp", + symbol: "IDR", + symbol_first: true, + }, + CHF : { + code: "CHF", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Swiss Franc", + // symbol: "Fr", + symbol: "CHF", + symbol_first: true, + }, + SEK : { + code: "SEK", + exponent: 2, + locale: EnBy, + minor_units: 100, + name: "Swedish Krona", + // symbol: "kr", + symbol: "SEK", + symbol_first: false, + }, + NOK : { + code: "NOK", + exponent: 2, + locale: EnUs, + minor_units: 100, + name: "Norwegian Krone", + // symbol: "kr", + symbol: "NOK", + symbol_first: false, + }, + MEXICAN_PESO : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Mexican Peso", + symbol: "MX$", + symbol_first: true, + }, + ZAR : { + code: "ZAR", + exponent: 2, + locale: EnUs, + minor_units: 10, + name: "South African Rand", + // symbol: "R", + symbol: "ZAR", + symbol_first: true, + }, + DKK : { + code: "DKK", + exponent: 2, + locale: EnEu, + minor_units: 50, + name: "Danish Krone", + // symbol: "kr.", + symbol: "DKK", + symbol_first: false, + }, + THB : { + code: "THB", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Thai Baht", + // symbol: "฿", + symbol: "THB", + symbol_first: true, + }, + HUF : { + code: "HUF", + exponent: 0, + locale: EnBy, + minor_units: 5, + name: "Hungarian Forint", + // symbol: "Ft", + symbol: "HUF", + symbol_first: false, + }, + KORUNA : { + code: "CZK", + exponent: 2, + locale: EnBy, + minor_units: 100, + name: "Czech Koruna", + // symbol: "Kč", + symbol: "CZK", + symbol_first: false, + }, + SHEKEL : { + code: "CZK", + exponent: 2, + locale: EnBy, + minor_units: 100, + name: "Czech Koruna", + symbol: "Kč", + symbol_first: false, + }, + CLP : { + code: "CLP", + exponent: 0, + locale: EnEu, + minor_units: 1, + name: "Chilean Peso", + // symbol: "$", + symbol: "CLP", + symbol_first: true, + }, + PHP : { + code: "PHP", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Philippine Peso", + symbol: "₱", + symbol_first: true, + }, + AED : { + code: "AED", + exponent: 2, + locale: EnUs, + minor_units: 25, + name: "United Arab Emirates Dirham", + // symbol: "د.إ", + symbol: "AED", + symbol_first: false, + }, + COP : { + code: "COP", + exponent: 2, + locale: EnEu, + minor_units: 20, + name: "Colombian Peso", + // symbol: "$", + symbol: "COP", + symbol_first: true, + }, + SAR : { + code: "SAR", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Saudi Riyal", + // symbol: "ر.س", + symbol: "SAR", + symbol_first: true, + }, + MYR : { + code: "MYR", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Malaysian Ringgit", + // symbol: "RM", + symbol: "MYR", + symbol_first: true, + }, + RON : { + code: "RON", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Romanian Leu", + // symbol: "ر.ق", + symbol: "RON", + symbol_first: false, + }, + ARS : { + code: "ARS", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Argentine Peso", + // symbol: "$", + symbol: "ARS", + symbol_first: true, + }, + UYU : { + code: "UYU", + exponent: 2, + locale: EnEu, + minor_units: 100, + name: "Uruguayan Peso", + // symbol: "$U", + symbol: "UYU", + symbol_first: true, + } + } +); + +impl NumberFormat { + pub fn currency(&self) -> &'static number_currency::Currency { + match self { + NumberFormat::Num => number_currency::NUMBER, + NumberFormat::USD => number_currency::USD, + NumberFormat::CanadianDollar => number_currency::CANADIAN_DOLLAR, + NumberFormat::EUR => number_currency::EUR, + NumberFormat::Pound => number_currency::GIP, + NumberFormat::Yen => number_currency::CNY, + NumberFormat::Ruble => number_currency::RUB, + NumberFormat::Rupee => number_currency::INR, + NumberFormat::Won => number_currency::KRW, + NumberFormat::Yuan => number_currency::YUAN, + NumberFormat::Real => number_currency::BRL, + NumberFormat::Lira => number_currency::TRY, + NumberFormat::Rupiah => number_currency::IDR, + NumberFormat::Franc => number_currency::CHF, + NumberFormat::HongKongDollar => number_currency::HONG_KONG_DOLLAR, + NumberFormat::NewZealandDollar => number_currency::NEW_ZEALAND_DOLLAR, + NumberFormat::Krona => number_currency::SEK, + NumberFormat::NorwegianKrone => number_currency::NOK, + NumberFormat::MexicanPeso => number_currency::MEXICAN_PESO, + NumberFormat::Rand => number_currency::ZAR, + NumberFormat::NewTaiwanDollar => number_currency::NEW_TAIWAN_DOLLAR, + NumberFormat::DanishKrone => number_currency::DKK, + NumberFormat::Baht => number_currency::THB, + NumberFormat::Forint => number_currency::HUF, + NumberFormat::Koruna => number_currency::KORUNA, + NumberFormat::Shekel => number_currency::SHEKEL, + NumberFormat::ChileanPeso => number_currency::CLP, + NumberFormat::PhilippinePeso => number_currency::PHP, + NumberFormat::Dirham => number_currency::AED, + NumberFormat::ColombianPeso => number_currency::COP, + NumberFormat::Riyal => number_currency::SAR, + NumberFormat::Ringgit => number_currency::MYR, + NumberFormat::Leu => number_currency::RON, + NumberFormat::ArgentinePeso => number_currency::ARS, + NumberFormat::UruguayanPeso => number_currency::UYU, + NumberFormat::Percent => number_currency::PERCENT, + } + } + + pub fn symbol(&self) -> String { + self.currency().symbol.to_string() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs new file mode 100644 index 0000000000..8136fb57c5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs @@ -0,0 +1,10 @@ +#![allow(clippy::module_inception)] +mod format; +mod number_filter; +mod number_tests; +mod number_type_option; +mod number_type_option_entities; + +pub use format::*; +pub use number_type_option::*; +pub use number_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs new file mode 100644 index 0000000000..bb59562efd --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs @@ -0,0 +1,83 @@ +use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; +use crate::services::field::NumberCellFormat; +use rust_decimal::prelude::Zero; +use rust_decimal::Decimal; +use std::str::FromStr; + +impl NumberFilterPB { + pub fn is_visible(&self, num_cell_data: &NumberCellFormat) -> bool { + if self.content.is_empty() { + match self.condition { + NumberFilterConditionPB::NumberIsEmpty => { + return num_cell_data.is_empty(); + }, + NumberFilterConditionPB::NumberIsNotEmpty => { + return !num_cell_data.is_empty(); + }, + _ => {}, + } + } + match num_cell_data.decimal().as_ref() { + None => false, + Some(cell_decimal) => { + let decimal = Decimal::from_str(&self.content).unwrap_or_else(|_| Decimal::zero()); + match self.condition { + NumberFilterConditionPB::Equal => cell_decimal == &decimal, + NumberFilterConditionPB::NotEqual => cell_decimal != &decimal, + NumberFilterConditionPB::GreaterThan => cell_decimal > &decimal, + NumberFilterConditionPB::LessThan => cell_decimal < &decimal, + NumberFilterConditionPB::GreaterThanOrEqualTo => cell_decimal >= &decimal, + NumberFilterConditionPB::LessThanOrEqualTo => cell_decimal <= &decimal, + _ => true, + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; + use crate::services::field::{NumberCellFormat, NumberFormat}; + #[test] + fn number_filter_equal_test() { + let number_filter = NumberFilterPB { + condition: NumberFilterConditionPB::Equal, + content: "123".to_owned(), + }; + + for (num_str, visible) in [("123", true), ("1234", false), ("", false)] { + let data = NumberCellFormat::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); + assert_eq!(number_filter.is_visible(&data), visible); + } + + let format = NumberFormat::USD; + for (num_str, visible) in [("$123", true), ("1234", false), ("", false)] { + let data = NumberCellFormat::from_format_str(num_str, true, &format).unwrap(); + assert_eq!(number_filter.is_visible(&data), visible); + } + } + #[test] + fn number_filter_greater_than_test() { + let number_filter = NumberFilterPB { + condition: NumberFilterConditionPB::GreaterThan, + content: "12".to_owned(), + }; + for (num_str, visible) in [("123", true), ("10", false), ("30", true), ("", false)] { + let data = NumberCellFormat::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); + assert_eq!(number_filter.is_visible(&data), visible); + } + } + + #[test] + fn number_filter_less_than_test() { + let number_filter = NumberFilterPB { + condition: NumberFilterConditionPB::LessThan, + content: "100".to_owned(), + }; + for (num_str, visible) in [("12", true), ("1234", false), ("30", true), ("", false)] { + let data = NumberCellFormat::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); + assert_eq!(number_filter.is_visible(&data), visible); + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs new file mode 100644 index 0000000000..aab0d6cf69 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs @@ -0,0 +1,672 @@ +#[cfg(test)] +mod tests { + use collab_database::fields::Field; + use strum::IntoEnumIterator; + + use crate::entities::FieldType; + use crate::services::cell::CellDataDecoder; + use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption}; + use crate::services::field::{FieldBuilder, NumberCellData}; + + /// Testing when the input is not a number. + #[test] + fn number_type_option_invalid_input_test() { + let type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(field_type.clone()).build(); + + // Input is empty String + assert_number(&type_option, "", "", &field_type, &field_rev); + + // Input is letter + assert_number(&type_option, "abc", "", &field_type, &field_rev); + } + + /// Testing the strip_currency_symbol function. It should return the string without the input symbol. + #[test] + fn number_type_option_strip_symbol_test() { + // Remove the $ symbol + assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned()); + // Remove the ¥ symbol + assert_eq!(strip_currency_symbol("¥0.2"), "0.2".to_owned()); + } + + /// Format the input number to the corresponding format string. + #[test] + fn number_type_option_format_number_test() { + let mut type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(field_type.clone()).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + }, + NumberFormat::USD => { + assert_number(&type_option, "18443", "$18,443", &field_type, &field_rev); + }, + NumberFormat::CanadianDollar => { + assert_number(&type_option, "18443", "CA$18,443", &field_type, &field_rev) + }, + NumberFormat::EUR => { + assert_number(&type_option, "18443", "€18.443", &field_type, &field_rev) + }, + NumberFormat::Pound => { + assert_number(&type_option, "18443", "£18,443", &field_type, &field_rev) + }, + + NumberFormat::Yen => { + assert_number(&type_option, "18443", "¥18,443", &field_type, &field_rev); + }, + NumberFormat::Ruble => { + assert_number(&type_option, "18443", "18.443RUB", &field_type, &field_rev) + }, + NumberFormat::Rupee => { + assert_number(&type_option, "18443", "₹18,443", &field_type, &field_rev) + }, + NumberFormat::Won => { + assert_number(&type_option, "18443", "₩18,443", &field_type, &field_rev) + }, + + NumberFormat::Yuan => { + assert_number(&type_option, "18443", "CN¥18,443", &field_type, &field_rev); + }, + NumberFormat::Real => { + assert_number(&type_option, "18443", "R$18,443", &field_type, &field_rev); + }, + NumberFormat::Lira => { + assert_number(&type_option, "18443", "TRY18.443", &field_type, &field_rev) + }, + NumberFormat::Rupiah => { + assert_number(&type_option, "18443", "IDR18,443", &field_type, &field_rev) + }, + NumberFormat::Franc => { + assert_number(&type_option, "18443", "CHF18,443", &field_type, &field_rev) + }, + NumberFormat::HongKongDollar => { + assert_number(&type_option, "18443", "HZ$18,443", &field_type, &field_rev) + }, + NumberFormat::NewZealandDollar => { + assert_number(&type_option, "18443", "NZ$18,443", &field_type, &field_rev) + }, + NumberFormat::Krona => { + assert_number(&type_option, "18443", "18 443SEK", &field_type, &field_rev) + }, + NumberFormat::NorwegianKrone => { + assert_number(&type_option, "18443", "18,443NOK", &field_type, &field_rev) + }, + NumberFormat::MexicanPeso => { + assert_number(&type_option, "18443", "MX$18,443", &field_type, &field_rev) + }, + NumberFormat::Rand => { + assert_number(&type_option, "18443", "ZAR18,443", &field_type, &field_rev) + }, + NumberFormat::NewTaiwanDollar => { + assert_number(&type_option, "18443", "NT$18,443", &field_type, &field_rev) + }, + NumberFormat::DanishKrone => { + assert_number(&type_option, "18443", "18.443DKK", &field_type, &field_rev) + }, + NumberFormat::Baht => { + assert_number(&type_option, "18443", "THB18,443", &field_type, &field_rev) + }, + NumberFormat::Forint => { + assert_number(&type_option, "18443", "18 443HUF", &field_type, &field_rev) + }, + NumberFormat::Koruna => { + assert_number(&type_option, "18443", "18 443CZK", &field_type, &field_rev) + }, + NumberFormat::Shekel => { + assert_number(&type_option, "18443", "18 443Kč", &field_type, &field_rev) + }, + NumberFormat::ChileanPeso => { + assert_number(&type_option, "18443", "CLP18.443", &field_type, &field_rev) + }, + NumberFormat::PhilippinePeso => { + assert_number(&type_option, "18443", "₱18,443", &field_type, &field_rev) + }, + NumberFormat::Dirham => { + assert_number(&type_option, "18443", "18,443AED", &field_type, &field_rev) + }, + NumberFormat::ColombianPeso => { + assert_number(&type_option, "18443", "COP18.443", &field_type, &field_rev) + }, + NumberFormat::Riyal => { + assert_number(&type_option, "18443", "SAR18,443", &field_type, &field_rev) + }, + NumberFormat::Ringgit => { + assert_number(&type_option, "18443", "MYR18,443", &field_type, &field_rev) + }, + NumberFormat::Leu => { + assert_number(&type_option, "18443", "18.443RON", &field_type, &field_rev) + }, + NumberFormat::ArgentinePeso => { + assert_number(&type_option, "18443", "ARS18.443", &field_type, &field_rev) + }, + NumberFormat::UruguayanPeso => { + assert_number(&type_option, "18443", "UYU18.443", &field_type, &field_rev) + }, + NumberFormat::Percent => { + assert_number(&type_option, "18443", "18,443%", &field_type, &field_rev) + }, + } + } + } + + /// Format the input String to the corresponding format string. + #[test] + fn number_type_option_format_str_test() { + let mut type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(field_type.clone()).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + assert_number(&type_option, "0.2", "0.2", &field_type, &field_rev); + assert_number(&type_option, "", "", &field_type, &field_rev); + assert_number(&type_option, "abc", "", &field_type, &field_rev); + }, + NumberFormat::USD => { + assert_number(&type_option, "$18,44", "$1,844", &field_type, &field_rev); + assert_number(&type_option, "$0.2", "$0.2", &field_type, &field_rev); + assert_number(&type_option, "$1844", "$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "$1,844", &field_type, &field_rev); + }, + NumberFormat::CanadianDollar => { + assert_number( + &type_option, + "CA$18,44", + "CA$1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "CA$0.2", "CA$0.2", &field_type, &field_rev); + assert_number(&type_option, "CA$1844", "CA$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "CA$1,844", &field_type, &field_rev); + }, + NumberFormat::EUR => { + assert_number(&type_option, "€18.44", "€18,44", &field_type, &field_rev); + assert_number(&type_option, "€0.5", "€0,5", &field_type, &field_rev); + assert_number(&type_option, "€1844", "€1.844", &field_type, &field_rev); + assert_number(&type_option, "1844", "€1.844", &field_type, &field_rev); + }, + NumberFormat::Pound => { + assert_number(&type_option, "£18,44", "£1,844", &field_type, &field_rev); + assert_number(&type_option, "£0.2", "£0.2", &field_type, &field_rev); + assert_number(&type_option, "£1844", "£1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "£1,844", &field_type, &field_rev); + }, + NumberFormat::Yen => { + assert_number(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev); + assert_number(&type_option, "¥0.2", "¥0.2", &field_type, &field_rev); + assert_number(&type_option, "¥1844", "¥1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "¥1,844", &field_type, &field_rev); + }, + NumberFormat::Ruble => { + assert_number( + &type_option, + "RUB18.44", + "18,44RUB", + &field_type, + &field_rev, + ); + assert_number(&type_option, "0.5", "0,5RUB", &field_type, &field_rev); + assert_number(&type_option, "RUB1844", "1.844RUB", &field_type, &field_rev); + assert_number(&type_option, "1844", "1.844RUB", &field_type, &field_rev); + }, + NumberFormat::Rupee => { + assert_number(&type_option, "₹18,44", "₹1,844", &field_type, &field_rev); + assert_number(&type_option, "₹0.2", "₹0.2", &field_type, &field_rev); + assert_number(&type_option, "₹1844", "₹1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "₹1,844", &field_type, &field_rev); + }, + NumberFormat::Won => { + assert_number(&type_option, "₩18,44", "₩1,844", &field_type, &field_rev); + assert_number(&type_option, "₩0.3", "₩0", &field_type, &field_rev); + assert_number(&type_option, "₩1844", "₩1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "₩1,844", &field_type, &field_rev); + }, + NumberFormat::Yuan => { + assert_number( + &type_option, + "CN¥18,44", + "CN¥1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "CN¥0.2", "CN¥0.2", &field_type, &field_rev); + assert_number(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "CN¥1,844", &field_type, &field_rev); + }, + NumberFormat::Real => { + assert_number(&type_option, "R$18,44", "R$1,844", &field_type, &field_rev); + assert_number(&type_option, "R$0.2", "R$0.2", &field_type, &field_rev); + assert_number(&type_option, "R$1844", "R$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "R$1,844", &field_type, &field_rev); + }, + NumberFormat::Lira => { + assert_number( + &type_option, + "TRY18.44", + "TRY18,44", + &field_type, + &field_rev, + ); + assert_number(&type_option, "TRY0.5", "TRY0,5", &field_type, &field_rev); + assert_number(&type_option, "TRY1844", "TRY1.844", &field_type, &field_rev); + assert_number(&type_option, "1844", "TRY1.844", &field_type, &field_rev); + }, + NumberFormat::Rupiah => { + assert_number( + &type_option, + "IDR18,44", + "IDR1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "IDR0.2", "IDR0.2", &field_type, &field_rev); + assert_number(&type_option, "IDR1844", "IDR1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "IDR1,844", &field_type, &field_rev); + }, + NumberFormat::Franc => { + assert_number( + &type_option, + "CHF18,44", + "CHF1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "CHF0.2", "CHF0.2", &field_type, &field_rev); + assert_number(&type_option, "CHF1844", "CHF1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "CHF1,844", &field_type, &field_rev); + }, + NumberFormat::HongKongDollar => { + assert_number( + &type_option, + "HZ$18,44", + "HZ$1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "HZ$0.2", "HZ$0.2", &field_type, &field_rev); + assert_number(&type_option, "HZ$1844", "HZ$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "HZ$1,844", &field_type, &field_rev); + }, + NumberFormat::NewZealandDollar => { + assert_number( + &type_option, + "NZ$18,44", + "NZ$1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "NZ$0.2", "NZ$0.2", &field_type, &field_rev); + assert_number(&type_option, "NZ$1844", "NZ$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "NZ$1,844", &field_type, &field_rev); + }, + NumberFormat::Krona => { + assert_number( + &type_option, + "SEK18,44", + "18,44SEK", + &field_type, + &field_rev, + ); + assert_number(&type_option, "SEK0.2", "0,2SEK", &field_type, &field_rev); + assert_number(&type_option, "SEK1844", "1 844SEK", &field_type, &field_rev); + assert_number(&type_option, "1844", "1 844SEK", &field_type, &field_rev); + }, + NumberFormat::NorwegianKrone => { + assert_number( + &type_option, + "NOK18,44", + "1,844NOK", + &field_type, + &field_rev, + ); + assert_number(&type_option, "NOK0.2", "0.2NOK", &field_type, &field_rev); + assert_number(&type_option, "NOK1844", "1,844NOK", &field_type, &field_rev); + assert_number(&type_option, "1844", "1,844NOK", &field_type, &field_rev); + }, + NumberFormat::MexicanPeso => { + assert_number( + &type_option, + "MX$18,44", + "MX$1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "MX$0.2", "MX$0.2", &field_type, &field_rev); + assert_number(&type_option, "MX$1844", "MX$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "MX$1,844", &field_type, &field_rev); + }, + NumberFormat::Rand => { + assert_number( + &type_option, + "ZAR18,44", + "ZAR1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "ZAR0.2", "ZAR0.2", &field_type, &field_rev); + assert_number(&type_option, "ZAR1844", "ZAR1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "ZAR1,844", &field_type, &field_rev); + }, + NumberFormat::NewTaiwanDollar => { + assert_number( + &type_option, + "NT$18,44", + "NT$1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "NT$0.2", "NT$0.2", &field_type, &field_rev); + assert_number(&type_option, "NT$1844", "NT$1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "NT$1,844", &field_type, &field_rev); + }, + NumberFormat::DanishKrone => { + assert_number( + &type_option, + "DKK18.44", + "18,44DKK", + &field_type, + &field_rev, + ); + assert_number(&type_option, "DKK0.5", "0,5DKK", &field_type, &field_rev); + assert_number(&type_option, "DKK1844", "1.844DKK", &field_type, &field_rev); + assert_number(&type_option, "1844", "1.844DKK", &field_type, &field_rev); + }, + NumberFormat::Baht => { + assert_number( + &type_option, + "THB18,44", + "THB1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "THB0.2", "THB0.2", &field_type, &field_rev); + assert_number(&type_option, "THB1844", "THB1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "THB1,844", &field_type, &field_rev); + }, + NumberFormat::Forint => { + assert_number(&type_option, "HUF18,44", "18HUF", &field_type, &field_rev); + assert_number(&type_option, "HUF0.3", "0HUF", &field_type, &field_rev); + assert_number(&type_option, "HUF1844", "1 844HUF", &field_type, &field_rev); + assert_number(&type_option, "1844", "1 844HUF", &field_type, &field_rev); + }, + NumberFormat::Koruna => { + assert_number( + &type_option, + "CZK18,44", + "18,44CZK", + &field_type, + &field_rev, + ); + assert_number(&type_option, "CZK0.2", "0,2CZK", &field_type, &field_rev); + assert_number(&type_option, "CZK1844", "1 844CZK", &field_type, &field_rev); + assert_number(&type_option, "1844", "1 844CZK", &field_type, &field_rev); + }, + NumberFormat::Shekel => { + assert_number(&type_option, "Kč18,44", "18,44Kč", &field_type, &field_rev); + assert_number(&type_option, "Kč0.2", "0,2Kč", &field_type, &field_rev); + assert_number(&type_option, "Kč1844", "1 844Kč", &field_type, &field_rev); + assert_number(&type_option, "1844", "1 844Kč", &field_type, &field_rev); + }, + NumberFormat::ChileanPeso => { + assert_number(&type_option, "CLP18.44", "CLP18", &field_type, &field_rev); + assert_number(&type_option, "0.5", "CLP0", &field_type, &field_rev); + assert_number(&type_option, "CLP1844", "CLP1.844", &field_type, &field_rev); + assert_number(&type_option, "1844", "CLP1.844", &field_type, &field_rev); + }, + NumberFormat::PhilippinePeso => { + assert_number(&type_option, "₱18,44", "₱1,844", &field_type, &field_rev); + assert_number(&type_option, "₱0.2", "₱0.2", &field_type, &field_rev); + assert_number(&type_option, "₱1844", "₱1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "₱1,844", &field_type, &field_rev); + }, + NumberFormat::Dirham => { + assert_number( + &type_option, + "AED18,44", + "1,844AED", + &field_type, + &field_rev, + ); + assert_number(&type_option, "AED0.2", "0.2AED", &field_type, &field_rev); + assert_number(&type_option, "AED1844", "1,844AED", &field_type, &field_rev); + assert_number(&type_option, "1844", "1,844AED", &field_type, &field_rev); + }, + NumberFormat::ColombianPeso => { + assert_number( + &type_option, + "COP18.44", + "COP18,44", + &field_type, + &field_rev, + ); + assert_number(&type_option, "0.5", "COP0,5", &field_type, &field_rev); + assert_number(&type_option, "COP1844", "COP1.844", &field_type, &field_rev); + assert_number(&type_option, "1844", "COP1.844", &field_type, &field_rev); + }, + NumberFormat::Riyal => { + assert_number( + &type_option, + "SAR18,44", + "SAR1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "SAR0.2", "SAR0.2", &field_type, &field_rev); + assert_number(&type_option, "SAR1844", "SAR1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "SAR1,844", &field_type, &field_rev); + }, + + NumberFormat::Ringgit => { + assert_number( + &type_option, + "MYR18,44", + "MYR1,844", + &field_type, + &field_rev, + ); + assert_number(&type_option, "MYR0.2", "MYR0.2", &field_type, &field_rev); + assert_number(&type_option, "MYR1844", "MYR1,844", &field_type, &field_rev); + assert_number(&type_option, "1844", "MYR1,844", &field_type, &field_rev); + }, + NumberFormat::Leu => { + assert_number( + &type_option, + "RON18.44", + "18,44RON", + &field_type, + &field_rev, + ); + assert_number(&type_option, "0.5", "0,5RON", &field_type, &field_rev); + assert_number(&type_option, "RON1844", "1.844RON", &field_type, &field_rev); + assert_number(&type_option, "1844", "1.844RON", &field_type, &field_rev); + }, + NumberFormat::ArgentinePeso => { + assert_number( + &type_option, + "ARS18.44", + "ARS18,44", + &field_type, + &field_rev, + ); + assert_number(&type_option, "0.5", "ARS0,5", &field_type, &field_rev); + assert_number(&type_option, "ARS1844", "ARS1.844", &field_type, &field_rev); + assert_number(&type_option, "1844", "ARS1.844", &field_type, &field_rev); + }, + NumberFormat::UruguayanPeso => { + assert_number( + &type_option, + "UYU18.44", + "UYU18,44", + &field_type, + &field_rev, + ); + assert_number(&type_option, "0.5", "UYU0,5", &field_type, &field_rev); + assert_number(&type_option, "UYU1844", "UYU1.844", &field_type, &field_rev); + assert_number(&type_option, "1844", "UYU1.844", &field_type, &field_rev); + }, + NumberFormat::Percent => { + assert_number(&type_option, "1", "1%", &field_type, &field_rev); + assert_number(&type_option, "10.1", "10.1%", &field_type, &field_rev); + assert_number(&type_option, "100", "100%", &field_type, &field_rev); + }, + } + } + } + + /// Carry out the sign positive to input number + #[test] + fn number_description_sign_test() { + let mut type_option = NumberTypeOption { + sign_positive: false, + ..Default::default() + }; + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(field_type.clone()).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + }, + NumberFormat::USD => { + assert_number(&type_option, "18443", "-$18,443", &field_type, &field_rev); + }, + NumberFormat::CanadianDollar => { + assert_number(&type_option, "18443", "-CA$18,443", &field_type, &field_rev) + }, + NumberFormat::EUR => { + assert_number(&type_option, "18443", "-€18.443", &field_type, &field_rev) + }, + NumberFormat::Pound => { + assert_number(&type_option, "18443", "-£18,443", &field_type, &field_rev) + }, + + NumberFormat::Yen => { + assert_number(&type_option, "18443", "-¥18,443", &field_type, &field_rev); + }, + NumberFormat::Ruble => { + assert_number(&type_option, "18443", "-18.443RUB", &field_type, &field_rev) + }, + NumberFormat::Rupee => { + assert_number(&type_option, "18443", "-₹18,443", &field_type, &field_rev) + }, + NumberFormat::Won => { + assert_number(&type_option, "18443", "-₩18,443", &field_type, &field_rev) + }, + + NumberFormat::Yuan => { + assert_number(&type_option, "18443", "-CN¥18,443", &field_type, &field_rev); + }, + NumberFormat::Real => { + assert_number(&type_option, "18443", "-R$18,443", &field_type, &field_rev); + }, + NumberFormat::Lira => { + assert_number(&type_option, "18443", "-TRY18.443", &field_type, &field_rev) + }, + NumberFormat::Rupiah => { + assert_number(&type_option, "18443", "-IDR18,443", &field_type, &field_rev) + }, + NumberFormat::Franc => { + assert_number(&type_option, "18443", "-CHF18,443", &field_type, &field_rev) + }, + NumberFormat::HongKongDollar => { + assert_number(&type_option, "18443", "-HZ$18,443", &field_type, &field_rev) + }, + NumberFormat::NewZealandDollar => { + assert_number(&type_option, "18443", "-NZ$18,443", &field_type, &field_rev) + }, + NumberFormat::Krona => { + assert_number(&type_option, "18443", "-18 443SEK", &field_type, &field_rev) + }, + NumberFormat::NorwegianKrone => { + assert_number(&type_option, "18443", "-18,443NOK", &field_type, &field_rev) + }, + NumberFormat::MexicanPeso => { + assert_number(&type_option, "18443", "-MX$18,443", &field_type, &field_rev) + }, + NumberFormat::Rand => { + assert_number(&type_option, "18443", "-ZAR18,443", &field_type, &field_rev) + }, + NumberFormat::NewTaiwanDollar => { + assert_number(&type_option, "18443", "-NT$18,443", &field_type, &field_rev) + }, + NumberFormat::DanishKrone => { + assert_number(&type_option, "18443", "-18.443DKK", &field_type, &field_rev) + }, + NumberFormat::Baht => { + assert_number(&type_option, "18443", "-THB18,443", &field_type, &field_rev) + }, + NumberFormat::Forint => { + assert_number(&type_option, "18443", "-18 443HUF", &field_type, &field_rev) + }, + NumberFormat::Koruna => { + assert_number(&type_option, "18443", "-18 443CZK", &field_type, &field_rev) + }, + NumberFormat::Shekel => { + assert_number(&type_option, "18443", "-18 443Kč", &field_type, &field_rev) + }, + NumberFormat::ChileanPeso => { + assert_number(&type_option, "18443", "-CLP18.443", &field_type, &field_rev) + }, + NumberFormat::PhilippinePeso => { + assert_number(&type_option, "18443", "-₱18,443", &field_type, &field_rev) + }, + NumberFormat::Dirham => { + assert_number(&type_option, "18443", "-18,443AED", &field_type, &field_rev) + }, + NumberFormat::ColombianPeso => { + assert_number(&type_option, "18443", "-COP18.443", &field_type, &field_rev) + }, + NumberFormat::Riyal => { + assert_number(&type_option, "18443", "-SAR18,443", &field_type, &field_rev) + }, + NumberFormat::Ringgit => { + assert_number(&type_option, "18443", "-MYR18,443", &field_type, &field_rev) + }, + NumberFormat::Leu => { + assert_number(&type_option, "18443", "-18.443RON", &field_type, &field_rev) + }, + NumberFormat::ArgentinePeso => { + assert_number(&type_option, "18443", "-ARS18.443", &field_type, &field_rev) + }, + NumberFormat::UruguayanPeso => { + assert_number(&type_option, "18443", "-UYU18.443", &field_type, &field_rev) + }, + NumberFormat::Percent => { + assert_number(&type_option, "18443", "-18,443%", &field_type, &field_rev) + }, + } + } + } + + fn assert_number( + type_option: &NumberTypeOption, + input_str: &str, + expected_str: &str, + field_type: &FieldType, + field_rev: &Field, + ) { + assert_eq!( + type_option + .decode_cell_str( + &NumberCellData(input_str.to_owned()).into(), + field_type, + field_rev + ) + .unwrap() + .to_string(), + expected_str.to_owned() + ); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs new file mode 100644 index 0000000000..1b2dc99500 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs @@ -0,0 +1,277 @@ +use crate::entities::{FieldType, NumberFilterPB}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::type_options::number_type_option::format::*; +use crate::services::field::{ + NumberCellFormat, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionTransform, CELL_DATE, +}; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; + +use crate::services::field::type_options::util::ProtobufStr; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use fancy_regex::Regex; +use flowy_error::FlowyResult; +use lazy_static::lazy_static; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::default::Default; +use std::str::FromStr; + +// Number +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NumberTypeOption { + pub format: NumberFormat, + pub scale: u32, + pub symbol: String, + pub sign_positive: bool, + pub name: String, +} + +#[derive(Clone, Debug, Default)] +pub struct NumberCellData(pub String); + +impl From<&Cell> for NumberCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_str_value(CELL_DATE).unwrap_or_default()) + } +} + +impl From for Cell { + fn from(data: NumberCellData) -> Self { + new_cell_builder(FieldType::Number) + .insert_str_value(CELL_DATE, data.0) + .build() + } +} + +impl std::convert::From for NumberCellData { + fn from(s: String) -> Self { + Self(s) + } +} + +impl ToString for NumberCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl TypeOption for NumberTypeOption { + type CellData = NumberCellData; + type CellChangeset = NumberCellChangeset; + type CellProtobufType = ProtobufStr; + type CellFilter = NumberFilterPB; +} + +impl From for NumberTypeOption { + fn from(data: TypeOptionData) -> Self { + let format = data + .get_i64_value("format") + .map(NumberFormat::from) + .unwrap_or_default(); + let scale = data.get_i64_value("scale").unwrap_or_default() as u32; + let symbol = data.get_str_value("symbol").unwrap_or_default(); + let sign_positive = data.get_bool_value("sign_positive").unwrap_or_default(); + let name = data.get_str_value("name").unwrap_or_default(); + Self { + format, + scale, + symbol, + sign_positive, + name, + } + } +} + +impl From for TypeOptionData { + fn from(data: NumberTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_i64_value("format", data.format.value()) + .insert_i64_value("scale", data.scale as i64) + .insert_bool_value("sign_positive", data.sign_positive) + .insert_str_value("name", data.name) + .insert_str_value("symbol", data.symbol) + .build() + } +} + +impl TypeOptionCellData for NumberTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + ProtobufStr::from(cell_data.0) + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(NumberCellData::from(cell)) + } +} + +impl NumberTypeOption { + pub fn new() -> Self { + Self::default() + } + + pub(crate) fn format_cell_data( + &self, + num_cell_data: &NumberCellData, + ) -> FlowyResult { + match self.format { + NumberFormat::Num => { + if SCIENTIFIC_NOTATION_REGEX + .is_match(&num_cell_data.0) + .unwrap() + { + match Decimal::from_scientific(&num_cell_data.0.to_lowercase()) { + Ok(value, ..) => Ok(NumberCellFormat::from_decimal(value)), + Err(_) => Ok(NumberCellFormat::new()), + } + } else { + let draw_numer_string = NUM_REGEX.replace_all(&num_cell_data.0, ""); + let strnum = match draw_numer_string.matches('.').count() { + 0 | 1 => draw_numer_string.to_string(), + _ => match EXTRACT_NUM_REGEX.captures(&draw_numer_string) { + Ok(captures) => match captures { + Some(capture) => capture[1].to_string(), + None => "".to_string(), + }, + Err(_) => "".to_string(), + }, + }; + match Decimal::from_str(&strnum) { + Ok(value, ..) => Ok(NumberCellFormat::from_decimal(value)), + Err(_) => Ok(NumberCellFormat::new()), + } + } + }, + _ => NumberCellFormat::from_format_str(&num_cell_data.0, self.sign_positive, &self.format), + } + } + + pub fn set_format(&mut self, format: NumberFormat) { + self.format = format; + self.symbol = format.symbol(); + } +} + +pub(crate) fn strip_currency_symbol(s: T) -> String { + let mut s = s.to_string(); + for symbol in CURRENCY_SYMBOL.iter() { + if s.starts_with(symbol) { + s = s.strip_prefix(symbol).unwrap_or("").to_string(); + break; + } + } + s +} + +impl TypeOptionTransform for NumberTypeOption {} + +impl CellDataDecoder for NumberTypeOption { + fn decode_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + if decoded_field_type.is_date() { + return Ok(Default::default()); + } + + let num_cell_data = self.decode_cell(cell)?; + Ok(NumberCellData::from( + self.format_cell_data(&num_cell_data)?.to_string(), + )) + } + + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String { + match self.format_cell_data(&cell_data) { + Ok(cell_data) => cell_data.to_string(), + Err(_) => "".to_string(), + } + } + + fn decode_cell_to_str(&self, cell: &Cell) -> String { + let cell_data = Self::CellData::from(cell); + self.decode_cell_data_to_str(cell_data) + } +} + +pub type NumberCellChangeset = String; + +impl CellDataChangeset for NumberTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let number_cell_data = NumberCellData(changeset.trim().to_string()); + let formatter = self.format_cell_data(&number_cell_data)?; + + match self.format { + NumberFormat::Num => Ok(( + NumberCellData(formatter.to_string()).into(), + NumberCellData::from(formatter.to_string()), + )), + _ => Ok(( + NumberCellData::from(formatter.to_string()).into(), + NumberCellData::from(formatter.to_string()), + )), + } + } +} + +impl TypeOptionCellDataFilter for NumberTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_number() { + return true; + } + match self.format_cell_data(cell_data) { + Ok(cell_data) => filter.is_visible(&cell_data), + Err(_) => true, + } + } +} + +impl TypeOptionCellDataCompare for NumberTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + cell_data.0.cmp(&other_cell_data.0) + } +} +impl std::default::Default for NumberTypeOption { + fn default() -> Self { + let format = NumberFormat::default(); + let symbol = format.symbol(); + NumberTypeOption { + format, + scale: 0, + symbol, + sign_positive: true, + name: "Number".to_string(), + } + } +} + +lazy_static! { + static ref NUM_REGEX: Regex = Regex::new(r"[^\d\.]").unwrap(); +} + +lazy_static! { + static ref SCIENTIFIC_NOTATION_REGEX: Regex = Regex::new(r"([+-]?\d*\.?\d+)e([+-]?\d+)").unwrap(); +} + +lazy_static! { + static ref EXTRACT_NUM_REGEX: Regex = Regex::new(r"^(\d+\.\d+)(?:\.\d+)*$").unwrap(); +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs new file mode 100644 index 0000000000..984c020f64 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs @@ -0,0 +1,126 @@ +use crate::services::cell::{CellBytesCustomParser, CellProtobufBlobParser, DecodedCellData}; +use crate::services::field::number_currency::Currency; +use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL}; +use bytes::Bytes; +use flowy_error::FlowyResult; +use rust_decimal::Decimal; +use rusty_money::Money; +use std::str::FromStr; + +#[derive(Default)] +pub struct NumberCellFormat { + decimal: Option, + money: Option, +} + +impl NumberCellFormat { + pub fn new() -> Self { + Self { + decimal: Default::default(), + money: None, + } + } + + pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult { + let mut num_str = strip_currency_symbol(s); + let currency = format.currency(); + if num_str.is_empty() { + return Ok(Self::default()); + } + match Decimal::from_str(&num_str) { + Ok(mut decimal) => { + decimal.set_sign_positive(sign_positive); + let money = Money::from_decimal(decimal, currency); + Ok(Self::from_money(money)) + }, + Err(_) => match Money::from_str(&num_str, currency) { + Ok(money) => Ok(NumberCellFormat::from_money(money)), + Err(_) => { + num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string())); + if num_str.chars().all(char::is_numeric) { + Self::from_format_str(&num_str, sign_positive, format) + } else { + // returns empty string if it can be formatted + Ok(Self::default()) + } + }, + }, + } + } + + pub fn from_decimal(decimal: Decimal) -> Self { + Self { + decimal: Some(decimal), + money: None, + } + } + + pub fn from_money(money: Money) -> Self { + Self { + decimal: Some(*money.amount()), + money: Some(money.to_string()), + } + } + + pub fn decimal(&self) -> &Option { + &self.decimal + } + + pub fn is_empty(&self) -> bool { + self.decimal.is_none() + } +} + +// impl FromStr for NumberCellData { +// type Err = FlowyError; +// +// fn from_str(s: &str) -> Result { +// if s.is_empty() { +// return Ok(Self::default()); +// } +// let decimal = Decimal::from_str(s).map_err(internal_error)?; +// Ok(Self::from_decimal(decimal)) +// } +// } + +impl ToString for NumberCellFormat { + fn to_string(&self) -> String { + match &self.money { + None => match self.decimal { + None => String::default(), + Some(decimal) => decimal.to_string(), + }, + Some(money) => money.to_string(), + } + } +} + +impl DecodedCellData for NumberCellFormat { + type Object = NumberCellFormat; + + fn is_empty(&self) -> bool { + self.decimal.is_none() + } +} + +pub struct NumberCellDataParser(); +impl CellProtobufBlobParser for NumberCellDataParser { + type Object = NumberCellFormat; + fn parser(bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => NumberCellFormat::from_format_str(&s, true, &NumberFormat::Num), + Err(_) => Ok(NumberCellFormat::default()), + } + } +} + +pub struct NumberCellCustomDataParser(pub NumberFormat); +impl CellBytesCustomParser for NumberCellCustomDataParser { + type Object = NumberCellFormat; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => NumberCellFormat::from_format_str(&s, true, &self.0), + Err(_) => Ok(NumberCellFormat::default()), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/checklist_filter.rs new file mode 100644 index 0000000000..c3086e2b54 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/checklist_filter.rs @@ -0,0 +1,40 @@ +use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB}; +use crate::services::field::{SelectOption, SelectedSelectOptions}; + +impl ChecklistFilterPB { + pub fn is_visible( + &self, + all_options: &[SelectOption], + selected_options: &SelectedSelectOptions, + ) -> bool { + let selected_option_ids = selected_options + .options + .iter() + .map(|option| option.id.as_str()) + .collect::>(); + + let mut all_option_ids = all_options + .iter() + .map(|option| option.id.as_str()) + .collect::>(); + + match self.condition { + ChecklistFilterConditionPB::IsComplete => { + if selected_option_ids.is_empty() { + return false; + } + + all_option_ids.retain(|option_id| !selected_option_ids.contains(option_id)); + all_option_ids.is_empty() + }, + ChecklistFilterConditionPB::IsIncomplete => { + if selected_option_ids.is_empty() { + return true; + } + + all_option_ids.retain(|option_id| !selected_option_ids.contains(option_id)); + !all_option_ids.is_empty() + }, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/checklist_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/checklist_type_option.rs new file mode 100644 index 0000000000..19a7d5d42b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/checklist_type_option.rs @@ -0,0 +1,143 @@ +use crate::entities::{ChecklistFilterPB, FieldType, SelectOptionCellDataPB}; +use crate::services::cell::CellDataChangeset; +use crate::services::field::{ + SelectOption, SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction, + SelectedSelectOptions, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, +}; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +// Multiple select +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct ChecklistTypeOption { + pub options: Vec, + pub disable_color: bool, +} + +impl TypeOption for ChecklistTypeOption { + type CellData = SelectOptionIds; + type CellChangeset = SelectOptionCellChangeset; + type CellProtobufType = SelectOptionCellDataPB; + type CellFilter = ChecklistFilterPB; +} + +impl From for ChecklistTypeOption { + fn from(data: TypeOptionData) -> Self { + data + .get_str_value("content") + .map(|s| serde_json::from_str::(&s).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From for TypeOptionData { + fn from(data: ChecklistTypeOption) -> Self { + let content = serde_json::to_string(&data).unwrap_or_default(); + TypeOptionDataBuilder::new() + .insert_str_value("content", content) + .build() + } +} + +impl TypeOptionCellData for ChecklistTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + self.get_selected_options(cell_data).into() + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(SelectOptionIds::from(cell)) + } +} + +impl SelectTypeOptionSharedAction for ChecklistTypeOption { + fn number_of_max_options(&self) -> Option { + None + } + + fn to_type_option_data(&self) -> TypeOptionData { + self.clone().into() + } + + fn options(&self) -> &Vec { + &self.options + } + + fn mut_options(&mut self) -> &mut Vec { + &mut self.options + } +} + +impl CellDataChangeset for ChecklistTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let insert_option_ids = changeset + .insert_option_ids + .into_iter() + .filter(|insert_option_id| { + self + .options + .iter() + .any(|option| &option.id == insert_option_id) + }) + .collect::>(); + + let select_option_ids = match cell { + None => SelectOptionIds::from(insert_option_ids), + Some(cell) => { + let mut select_ids = SelectOptionIds::from(&cell); + for insert_option_id in insert_option_ids { + if !select_ids.contains(&insert_option_id) { + select_ids.push(insert_option_id); + } + } + + for delete_option_id in changeset.delete_option_ids { + select_ids.retain(|id| id != &delete_option_id); + } + + select_ids + }, + }; + Ok(( + select_option_ids.to_cell_data(FieldType::Checklist), + select_option_ids, + )) + } +} +impl TypeOptionCellDataFilter for ChecklistTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_check_list() { + return true; + } + let selected_options = + SelectedSelectOptions::from(self.get_selected_options(cell_data.clone())); + filter.is_visible(&self.options, &selected_options) + } +} + +impl TypeOptionCellDataCompare for ChecklistTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + cell_data.len().cmp(&other_cell_data.len()) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs new file mode 100644 index 0000000000..a8f5acfafd --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs @@ -0,0 +1,17 @@ +mod checklist_filter; +mod checklist_type_option; +mod multi_select_type_option; +mod select_filter; +mod select_ids; +mod select_option; +mod select_type_option; +mod single_select_type_option; +mod type_option_transform; + +pub use checklist_filter::*; +pub use checklist_type_option::*; +pub use multi_select_type_option::*; +pub use select_ids::*; +pub use select_option::*; +pub use select_type_option::*; +pub use single_select_type_option::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs new file mode 100644 index 0000000000..3d945f527d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -0,0 +1,288 @@ +use std::cmp::{min, Ordering}; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use serde::{Deserialize, Serialize}; + +use flowy_error::FlowyResult; + +use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB}; +use crate::services::cell::CellDataChangeset; +use crate::services::field::{ + default_order, SelectOption, SelectOptionCellChangeset, SelectOptionIds, + SelectTypeOptionSharedAction, SelectedSelectOptions, TypeOption, TypeOptionCellData, + TypeOptionCellDataCompare, TypeOptionCellDataFilter, +}; + +// Multiple select +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct MultiSelectTypeOption { + pub options: Vec, + pub disable_color: bool, +} + +impl TypeOption for MultiSelectTypeOption { + type CellData = SelectOptionIds; + type CellChangeset = SelectOptionCellChangeset; + type CellProtobufType = SelectOptionCellDataPB; + type CellFilter = SelectOptionFilterPB; +} + +impl From for MultiSelectTypeOption { + fn from(data: TypeOptionData) -> Self { + data + .get_str_value("content") + .map(|s| serde_json::from_str::(&s).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From for TypeOptionData { + fn from(data: MultiSelectTypeOption) -> Self { + let content = serde_json::to_string(&data).unwrap_or_default(); + TypeOptionDataBuilder::new() + .insert_str_value("content", content) + .build() + } +} + +impl TypeOptionCellData for MultiSelectTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + self.get_selected_options(cell_data).into() + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(SelectOptionIds::from(cell)) + } +} + +impl SelectTypeOptionSharedAction for MultiSelectTypeOption { + fn number_of_max_options(&self) -> Option { + None + } + + fn to_type_option_data(&self) -> TypeOptionData { + self.clone().into() + } + + fn options(&self) -> &Vec { + &self.options + } + + fn mut_options(&mut self) -> &mut Vec { + &mut self.options + } +} + +impl CellDataChangeset for MultiSelectTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let insert_option_ids = changeset + .insert_option_ids + .into_iter() + .filter(|insert_option_id| { + self + .options + .iter() + .any(|option| &option.id == insert_option_id) + }) + .collect::>(); + + let select_option_ids = match cell { + None => SelectOptionIds::from(insert_option_ids), + Some(cell) => { + let mut select_ids = SelectOptionIds::from(&cell); + for insert_option_id in insert_option_ids { + if !select_ids.contains(&insert_option_id) { + select_ids.push(insert_option_id); + } + } + + for delete_option_id in changeset.delete_option_ids { + select_ids.retain(|id| id != &delete_option_id); + } + + tracing::trace!("Multi-select cell data: {}", select_ids.to_string()); + select_ids + }, + }; + Ok(( + select_option_ids.to_cell_data(FieldType::MultiSelect), + select_option_ids, + )) + } +} + +impl TypeOptionCellDataFilter for MultiSelectTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_multi_select() { + return true; + } + let selected_options = + SelectedSelectOptions::from(self.get_selected_options(cell_data.clone())); + filter.is_visible(&selected_options, FieldType::MultiSelect) + } +} + +impl TypeOptionCellDataCompare for MultiSelectTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + for i in 0..min(cell_data.len(), other_cell_data.len()) { + let order = match ( + cell_data + .get(i) + .and_then(|id| self.options.iter().find(|option| &option.id == id)), + other_cell_data + .get(i) + .and_then(|id| self.options.iter().find(|option| &option.id == id)), + ) { + (Some(left), Some(right)) => left.name.cmp(&right.name), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => default_order(), + }; + + if order.is_ne() { + return order; + } + } + default_order() + } +} + +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::CellDataChangeset; + use crate::services::field::type_options::selection_type_option::*; + use crate::services::field::MultiSelectTypeOption; + use crate::services::field::{CheckboxTypeOption, TypeOptionTransform}; + + #[test] + fn multi_select_transform_with_checkbox_type_option_test() { + let checkbox_type_option = CheckboxTypeOption { is_selected: false }; + let mut multi_select = MultiSelectTypeOption::default(); + multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.clone().into()); + debug_assert_eq!(multi_select.options.len(), 2); + + // Already contain the yes/no option. It doesn't need to insert new options + multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.into()); + debug_assert_eq!(multi_select.options.len(), 2); + } + + #[test] + fn multi_select_transform_with_single_select_type_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let single_select = SingleSelectTypeOption { + options: vec![google, facebook], + disable_color: false, + }; + let mut multi_select = MultiSelectTypeOption { + options: vec![], + disable_color: false, + }; + multi_select.transform_type_option(FieldType::MultiSelect, single_select.into()); + debug_assert_eq!(multi_select.options.len(), 2); + } + + // #[test] + + #[test] + fn multi_select_insert_multi_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let multi_select = MultiSelectTypeOption { + options: vec![google.clone(), facebook.clone()], + disable_color: false, + }; + + let option_ids = vec![google.id, facebook.id]; + let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone()); + let select_option_ids: SelectOptionIds = + multi_select.apply_changeset(changeset, None).unwrap().1; + + assert_eq!(&*select_option_ids, &option_ids); + } + + #[test] + fn multi_select_unselect_multi_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let multi_select = MultiSelectTypeOption { + options: vec![google.clone(), facebook.clone()], + disable_color: false, + }; + let option_ids = vec![google.id, facebook.id]; + + // insert + let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone()); + let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1; + assert_eq!(&*select_option_ids, &option_ids); + + // delete + let changeset = SelectOptionCellChangeset::from_delete_options(option_ids); + let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1; + assert!(select_option_ids.is_empty()); + } + + #[test] + fn multi_select_insert_single_option_test() { + let google = SelectOption::new("Google"); + let multi_select = MultiSelectTypeOption { + options: vec![google.clone()], + disable_color: false, + }; + + let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id); + let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1; + assert_eq!(select_option_ids.to_string(), google.id); + } + + #[test] + fn multi_select_insert_non_exist_option_test() { + let google = SelectOption::new("Google"); + let multi_select = MultiSelectTypeOption { + options: vec![], + disable_color: false, + }; + + let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id); + let (_, select_option_ids) = multi_select.apply_changeset(changeset, None).unwrap(); + assert!(select_option_ids.is_empty()); + } + + #[test] + fn multi_select_insert_invalid_option_id_test() { + let google = SelectOption::new("Google"); + let multi_select = MultiSelectTypeOption { + options: vec![google], + disable_color: false, + }; + + // empty option id string + let changeset = SelectOptionCellChangeset::from_insert_option_id(""); + let (cell, _) = multi_select.apply_changeset(changeset, None).unwrap(); + let option_ids = SelectOptionIds::from(&cell); + assert!(option_ids.is_empty()); + + let changeset = SelectOptionCellChangeset::from_insert_option_id("123,456"); + let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1; + assert!(select_option_ids.is_empty()); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs new file mode 100644 index 0000000000..3e20d58b0b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs @@ -0,0 +1,316 @@ +#![allow(clippy::needless_collect)] + +use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB}; +use crate::services::field::SelectedSelectOptions; + +impl SelectOptionFilterPB { + pub fn is_visible( + &self, + selected_options: &SelectedSelectOptions, + field_type: FieldType, + ) -> bool { + let selected_option_ids: Vec<&String> = selected_options + .options + .iter() + .map(|option| &option.id) + .collect(); + match self.condition { + SelectOptionConditionPB::OptionIs => match field_type { + FieldType::SingleSelect => { + if self.option_ids.is_empty() { + return true; + } + + if selected_options.options.is_empty() { + return false; + } + + let required_options = self + .option_ids + .iter() + .filter(|id| selected_option_ids.contains(id)) + .collect::>(); + + !required_options.is_empty() + }, + FieldType::MultiSelect => { + if self.option_ids.is_empty() { + return true; + } + + let required_options = self + .option_ids + .iter() + .filter(|id| selected_option_ids.contains(id)) + .collect::>(); + + !required_options.is_empty() + }, + _ => false, + }, + SelectOptionConditionPB::OptionIsNot => match field_type { + FieldType::SingleSelect => { + if self.option_ids.is_empty() { + return true; + } + + if selected_options.options.is_empty() { + return false; + } + + let required_options = self + .option_ids + .iter() + .filter(|id| selected_option_ids.contains(id)) + .collect::>(); + + required_options.is_empty() + }, + FieldType::MultiSelect => { + let required_options = self + .option_ids + .iter() + .filter(|id| selected_option_ids.contains(id)) + .collect::>(); + + required_options.is_empty() + }, + _ => false, + }, + SelectOptionConditionPB::OptionIsEmpty => selected_option_ids.is_empty(), + SelectOptionConditionPB::OptionIsNotEmpty => !selected_option_ids.is_empty(), + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::all)] + use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB}; + use crate::services::field::selection_type_option::SelectedSelectOptions; + use crate::services::field::SelectOption; + + #[test] + fn select_option_filter_is_empty_test() { + let option = SelectOption::new("A"); + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIsEmpty, + option_ids: vec![], + }; + + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { options: vec![] }, + FieldType::SingleSelect + ), + true + ); + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { + options: vec![option.clone()] + }, + FieldType::SingleSelect + ), + false, + ); + + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { options: vec![] }, + FieldType::MultiSelect + ), + true + ); + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { + options: vec![option] + }, + FieldType::MultiSelect + ), + false, + ); + } + + #[test] + fn select_option_filter_is_not_empty_test() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIsNotEmpty, + option_ids: vec![option_1.id.clone(), option_2.id.clone()], + }; + + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { + options: vec![option_1.clone()] + }, + FieldType::SingleSelect + ), + true + ); + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { options: vec![] }, + FieldType::SingleSelect + ), + false, + ); + + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { + options: vec![option_1.clone()] + }, + FieldType::MultiSelect + ), + true + ); + assert_eq!( + filter.is_visible( + &SelectedSelectOptions { options: vec![] }, + FieldType::MultiSelect + ), + false, + ); + } + + #[test] + fn single_select_option_filter_is_not_test() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let option_3 = SelectOption::new("C"); + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIsNot, + option_ids: vec![option_1.id.clone(), option_2.id.clone()], + }; + + for (options, is_visible) in vec![ + (vec![option_2.clone()], false), + (vec![option_1.clone()], false), + (vec![option_3.clone()], true), + (vec![option_1.clone(), option_2.clone()], false), + ] { + assert_eq!( + filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect), + is_visible + ); + } + } + + #[test] + fn single_select_option_filter_is_test() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let option_3 = SelectOption::new("c"); + + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![option_1.id.clone()], + }; + for (options, is_visible) in vec![ + (vec![option_1.clone()], true), + (vec![option_2.clone()], false), + (vec![option_3.clone()], false), + (vec![option_1.clone(), option_2.clone()], true), + ] { + assert_eq!( + filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect), + is_visible + ); + } + } + + #[test] + fn single_select_option_filter_is_test2() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![], + }; + for (options, is_visible) in vec![ + (vec![option_1.clone()], true), + (vec![option_2.clone()], true), + (vec![option_1.clone(), option_2.clone()], true), + ] { + assert_eq!( + filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect), + is_visible + ); + } + } + + #[test] + fn multi_select_option_filter_not_contains_test() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let option_3 = SelectOption::new("C"); + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIsNot, + option_ids: vec![option_1.id.clone(), option_2.id.clone()], + }; + + for (options, is_visible) in vec![ + (vec![option_1.clone(), option_2.clone()], false), + (vec![option_1.clone()], false), + (vec![option_2.clone()], false), + (vec![option_3.clone()], true), + ( + vec![option_1.clone(), option_2.clone(), option_3.clone()], + false, + ), + (vec![], true), + ] { + assert_eq!( + filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect), + is_visible + ); + } + } + #[test] + fn multi_select_option_filter_contains_test() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let option_3 = SelectOption::new("C"); + + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![option_1.id.clone(), option_2.id.clone()], + }; + for (options, is_visible) in vec![ + ( + vec![option_1.clone(), option_2.clone(), option_3.clone()], + true, + ), + (vec![option_2.clone(), option_1.clone()], true), + (vec![option_2.clone()], true), + (vec![option_1.clone(), option_3.clone()], true), + (vec![option_3.clone()], false), + ] { + assert_eq!( + filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect), + is_visible + ); + } + } + + #[test] + fn multi_select_option_filter_contains_test2() { + let option_1 = SelectOption::new("A"); + + let filter = SelectOptionFilterPB { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![], + }; + for (options, is_visible) in vec![(vec![option_1.clone()], true), (vec![], true)] { + assert_eq!( + filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect), + is_visible + ); + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs new file mode 100644 index 0000000000..b0721ee445 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs @@ -0,0 +1,110 @@ +use crate::entities::FieldType; +use crate::services::cell::{DecodedCellData, FromCellString}; +use crate::services::field::CELL_DATE; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use flowy_error::FlowyResult; + +pub const SELECTION_IDS_SEPARATOR: &str = ","; + +/// List of select option ids +/// +/// Calls [to_string] will return a string consists list of ids, +/// placing a commas separator between each +/// +#[derive(Default, Clone, Debug)] +pub struct SelectOptionIds(Vec); + +impl SelectOptionIds { + pub fn new() -> Self { + Self::default() + } + pub fn into_inner(self) -> Vec { + self.0 + } + + pub fn to_cell_data(&self, field_type: FieldType) -> Cell { + new_cell_builder(field_type) + .insert_str_value(CELL_DATE, self.to_string()) + .build() + } +} + +impl FromCellString for SelectOptionIds { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Ok(Self::from(s.to_owned())) + } +} + +impl From<&Cell> for SelectOptionIds { + fn from(cell: &Cell) -> Self { + let value = cell.get_str_value(CELL_DATE).unwrap_or_default(); + Self::from(value) + } +} + +impl std::convert::From for SelectOptionIds { + fn from(s: String) -> Self { + if s.is_empty() { + return Self(vec![]); + } + + let ids = s + .split(SELECTION_IDS_SEPARATOR) + .map(|id| id.to_string()) + .collect::>(); + Self(ids) + } +} + +impl std::convert::From> for SelectOptionIds { + fn from(ids: Vec) -> Self { + let ids = ids + .into_iter() + .filter(|id| !id.is_empty()) + .collect::>(); + Self(ids) + } +} + +impl ToString for SelectOptionIds { + /// Returns a string that consists list of ids, placing a commas + /// separator between each + fn to_string(&self) -> String { + self.0.join(SELECTION_IDS_SEPARATOR) + } +} + +impl std::convert::From> for SelectOptionIds { + fn from(s: Option) -> Self { + match s { + None => Self(vec![]), + Some(s) => Self::from(s), + } + } +} + +impl std::ops::Deref for SelectOptionIds { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for SelectOptionIds { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl DecodedCellData for SelectOptionIds { + type Object = SelectOptionIds; + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_option.rs new file mode 100644 index 0000000000..d79f81b255 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_option.rs @@ -0,0 +1,97 @@ +use crate::entities::SelectOptionCellDataPB; +use crate::services::field::SelectOptionIds; +use collab_database::database::gen_option_id; +use serde::{Deserialize, Serialize}; + +/// [SelectOption] represents an option for a single select, and multiple select. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SelectOption { + pub id: String, + pub name: String, + pub color: SelectOptionColor, +} + +impl SelectOption { + pub fn new(name: &str) -> Self { + SelectOption { + id: gen_option_id(), + name: name.to_owned(), + color: SelectOptionColor::default(), + } + } + + pub fn with_color(name: &str, color: SelectOptionColor) -> Self { + SelectOption { + id: gen_option_id(), + name: name.to_owned(), + color, + } + } +} + +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[repr(u8)] +pub enum SelectOptionColor { + Purple = 0, + Pink = 1, + LightPink = 2, + Orange = 3, + Yellow = 4, + Lime = 5, + Green = 6, + Aqua = 7, + Blue = 8, +} + +impl std::default::Default for SelectOptionColor { + fn default() -> Self { + SelectOptionColor::Purple + } +} + +#[derive(Debug)] +pub struct SelectOptionCellData { + pub options: Vec, + pub select_options: Vec, +} + +impl From for SelectOptionCellDataPB { + fn from(data: SelectOptionCellData) -> Self { + SelectOptionCellDataPB { + options: data + .options + .into_iter() + .map(|option| option.into()) + .collect(), + select_options: data + .select_options + .into_iter() + .map(|option| option.into()) + .collect(), + } + } +} + +pub fn make_selected_options(ids: SelectOptionIds, options: &[SelectOption]) -> Vec { + ids + .iter() + .flat_map(|option_id| { + options + .iter() + .find(|option| &option.id == option_id) + .cloned() + }) + .collect() +} + +pub struct SelectedSelectOptions { + pub(crate) options: Vec, +} + +impl std::convert::From for SelectedSelectOptions { + fn from(data: SelectOptionCellData) -> Self { + Self { + options: data.select_options, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs new file mode 100644 index 0000000000..b2f889c303 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -0,0 +1,281 @@ +use bytes::Bytes; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::Cell; +use serde::{Deserialize, Serialize}; + +use flowy_error::{internal_error, ErrorCode, FlowyResult}; + +use crate::entities::{FieldType, SelectOptionCellDataPB}; +use crate::services::cell::{ + CellDataDecoder, CellProtobufBlobParser, DecodedCellData, FromCellChangeset, ToCellChangeset, +}; +use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformHelper; +use crate::services::field::{ + make_selected_options, CheckboxCellData, ChecklistTypeOption, MultiSelectTypeOption, + SelectOption, SelectOptionCellData, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, + TypeOption, TypeOptionCellData, TypeOptionTransform, SELECTION_IDS_SEPARATOR, +}; + +/// Defines the shared actions used by SingleSelect or Multi-Select. +pub trait SelectTypeOptionSharedAction: Send + Sync { + /// Returns `None` means there is no limited + fn number_of_max_options(&self) -> Option; + + /// Insert the `SelectOption` into corresponding type option. + fn insert_option(&mut self, new_option: SelectOption) { + let options = self.mut_options(); + if let Some(index) = options + .iter() + .position(|option| option.id == new_option.id || option.name == new_option.name) + { + options.remove(index); + options.insert(index, new_option); + } else { + options.insert(0, new_option); + } + } + + fn delete_option(&mut self, delete_option: SelectOption) { + let options = self.mut_options(); + if let Some(index) = options + .iter() + .position(|option| option.id == delete_option.id) + { + options.remove(index); + } + } + + fn create_option(&self, name: &str) -> SelectOption { + let color = new_select_option_color(self.options()); + SelectOption::with_color(name, color) + } + + /// Return a list of options that are selected by user + fn get_selected_options(&self, ids: SelectOptionIds) -> SelectOptionCellData { + let mut select_options = make_selected_options(ids, self.options()); + match self.number_of_max_options() { + None => {}, + Some(number_of_max_options) => { + select_options.truncate(number_of_max_options); + }, + } + SelectOptionCellData { + options: self.options().clone(), + select_options, + } + } + + fn to_type_option_data(&self) -> TypeOptionData; + + fn options(&self) -> &Vec; + + fn mut_options(&mut self) -> &mut Vec; +} + +impl TypeOptionTransform for T +where + T: SelectTypeOptionSharedAction + TypeOption + CellDataDecoder, +{ + fn transformable(&self) -> bool { + true + } + + fn transform_type_option( + &mut self, + _old_type_option_field_type: FieldType, + _old_type_option_data: TypeOptionData, + ) { + SelectOptionTypeOptionTransformHelper::transform_type_option( + self, + &_old_type_option_field_type, + _old_type_option_data, + ); + } + + fn transform_type_option_cell( + &self, + cell: &Cell, + _decoded_field_type: &FieldType, + _field: &Field, + ) -> Option<::CellData> { + match _decoded_field_type { + FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => None, + FieldType::Checkbox => { + let cell_content = CheckboxCellData::from(cell).to_string(); + let mut transformed_ids = Vec::new(); + let options = self.options(); + if let Some(option) = options.iter().find(|option| option.name == cell_content) { + transformed_ids.push(option.id.clone()); + } + Some(SelectOptionIds::from(transformed_ids)) + }, + FieldType::RichText => Some(SelectOptionIds::from(cell)), + _ => Some(SelectOptionIds::from(vec![])), + } + } +} + +impl CellDataDecoder for T +where + T: SelectTypeOptionSharedAction + TypeOption + TypeOptionCellData, +{ + fn decode_cell_str( + &self, + cell: &Cell, + _decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + self.decode_cell(cell) + } + + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String { + self + .get_selected_options(cell_data) + .select_options + .into_iter() + .map(|option| option.name) + .collect::>() + .join(SELECTION_IDS_SEPARATOR) + } + + fn decode_cell_to_str(&self, cell: &Cell) -> String { + let cell_data = Self::CellData::from(cell); + self.decode_cell_data_to_str(cell_data) + } +} + +pub fn select_type_option_from_field( + field_rev: &Field, +) -> FlowyResult> { + let field_type = FieldType::from(field_rev.field_type); + match &field_type { + FieldType::SingleSelect => { + let type_option = field_rev + .get_type_option::(field_type) + .unwrap_or_default(); + Ok(Box::new(type_option)) + }, + FieldType::MultiSelect => { + let type_option = field_rev + .get_type_option::(&field_type) + .unwrap_or_default(); + Ok(Box::new(type_option)) + }, + FieldType::Checklist => { + let type_option = field_rev + .get_type_option::(&field_type) + .unwrap_or_default(); + Ok(Box::new(type_option)) + }, + ty => { + tracing::error!("Unsupported field type: {:?} for this handler", ty); + Err(ErrorCode::FieldInvalidOperation.into()) + }, + } +} + +pub fn new_select_option_color(options: &[SelectOption]) -> SelectOptionColor { + let mut freq: Vec = vec![0; 9]; + + for option in options { + freq[option.color.to_owned() as usize] += 1; + } + + match freq + .into_iter() + .enumerate() + .min_by_key(|(_, v)| *v) + .map(|(idx, _val)| idx) + .unwrap() + { + 0 => SelectOptionColor::Purple, + 1 => SelectOptionColor::Pink, + 2 => SelectOptionColor::LightPink, + 3 => SelectOptionColor::Orange, + 4 => SelectOptionColor::Yellow, + 5 => SelectOptionColor::Lime, + 6 => SelectOptionColor::Green, + 7 => SelectOptionColor::Aqua, + 8 => SelectOptionColor::Blue, + _ => SelectOptionColor::Purple, + } +} + +pub struct SelectOptionIdsParser(); +impl CellProtobufBlobParser for SelectOptionIdsParser { + type Object = SelectOptionIds; + fn parser(bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => Ok(SelectOptionIds::from(s)), + Err(_) => Ok(SelectOptionIds::from("".to_owned())), + } + } +} + +impl DecodedCellData for SelectOptionCellDataPB { + type Object = SelectOptionCellDataPB; + + fn is_empty(&self) -> bool { + self.select_options.is_empty() + } +} + +pub struct SelectOptionCellDataParser(); +impl CellProtobufBlobParser for SelectOptionCellDataParser { + type Object = SelectOptionCellDataPB; + + fn parser(bytes: &Bytes) -> FlowyResult { + SelectOptionCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} + +#[derive(Clone, Serialize, Deserialize, Default, Debug)] +pub struct SelectOptionCellChangeset { + pub insert_option_ids: Vec, + pub delete_option_ids: Vec, +} + +impl FromCellChangeset for SelectOptionCellChangeset { + fn from_changeset(changeset: String) -> FlowyResult + where + Self: Sized, + { + serde_json::from_str::(&changeset).map_err(internal_error) + } +} + +impl ToCellChangeset for SelectOptionCellChangeset { + fn to_cell_changeset_str(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } +} + +impl SelectOptionCellChangeset { + pub fn from_insert_option_id(option_id: &str) -> Self { + SelectOptionCellChangeset { + insert_option_ids: vec![option_id.to_string()], + delete_option_ids: vec![], + } + } + + pub fn from_insert_options(option_ids: Vec) -> Self { + SelectOptionCellChangeset { + insert_option_ids: option_ids, + delete_option_ids: vec![], + } + } + + pub fn from_delete_option_id(option_id: &str) -> Self { + SelectOptionCellChangeset { + insert_option_ids: vec![], + delete_option_ids: vec![option_id.to_string()], + } + } + + pub fn from_delete_options(option_ids: Vec) -> Self { + SelectOptionCellChangeset { + insert_option_ids: vec![], + delete_option_ids: option_ids, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs new file mode 100644 index 0000000000..68228832d2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -0,0 +1,224 @@ +use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB}; +use crate::services::cell::CellDataChangeset; +use crate::services::field::{ + default_order, SelectOption, SelectedSelectOptions, TypeOption, TypeOptionCellData, + TypeOptionCellDataCompare, TypeOptionCellDataFilter, +}; +use crate::services::field::{ + SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction, +}; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +// Single select +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SingleSelectTypeOption { + pub options: Vec, + pub disable_color: bool, +} + +impl TypeOption for SingleSelectTypeOption { + type CellData = SelectOptionIds; + type CellChangeset = SelectOptionCellChangeset; + type CellProtobufType = SelectOptionCellDataPB; + type CellFilter = SelectOptionFilterPB; +} + +impl From for SingleSelectTypeOption { + fn from(data: TypeOptionData) -> Self { + data + .get_str_value("content") + .map(|s| serde_json::from_str::(&s).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From for TypeOptionData { + fn from(data: SingleSelectTypeOption) -> Self { + let content = serde_json::to_string(&data).unwrap_or_default(); + TypeOptionDataBuilder::new() + .insert_str_value("content", content) + .build() + } +} + +impl TypeOptionCellData for SingleSelectTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + self.get_selected_options(cell_data).into() + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(SelectOptionIds::from(cell)) + } +} + +impl SelectTypeOptionSharedAction for SingleSelectTypeOption { + fn number_of_max_options(&self) -> Option { + Some(1) + } + + fn to_type_option_data(&self) -> TypeOptionData { + self.clone().into() + } + + fn options(&self) -> &Vec { + &self.options + } + + fn mut_options(&mut self) -> &mut Vec { + &mut self.options + } +} + +impl CellDataChangeset for SingleSelectTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let mut insert_option_ids = changeset + .insert_option_ids + .into_iter() + .filter(|insert_option_id| { + self + .options + .iter() + .any(|option| &option.id == insert_option_id) + }) + .collect::>(); + + // In single select, the insert_option_ids should only contain one select option id. + // Sometimes, the insert_option_ids may contain list of option ids. For example, + // copy/paste a ids string. + let select_option_ids = if insert_option_ids.is_empty() { + SelectOptionIds::from(insert_option_ids) + } else { + // Just take the first select option + let _ = insert_option_ids.drain(1..); + SelectOptionIds::from(insert_option_ids) + }; + Ok(( + select_option_ids.to_cell_data(FieldType::SingleSelect), + select_option_ids, + )) + } +} + +impl TypeOptionCellDataFilter for SingleSelectTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_single_select() { + return true; + } + let selected_options = + SelectedSelectOptions::from(self.get_selected_options(cell_data.clone())); + filter.is_visible(&selected_options, FieldType::SingleSelect) + } +} + +impl TypeOptionCellDataCompare for SingleSelectTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + match ( + cell_data + .first() + .and_then(|id| self.options.iter().find(|option| &option.id == id)), + other_cell_data + .first() + .and_then(|id| self.options.iter().find(|option| &option.id == id)), + ) { + (Some(left), Some(right)) => left.name.cmp(&right.name), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => default_order(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::CellDataChangeset; + use crate::services::field::type_options::*; + + #[test] + fn single_select_transform_with_checkbox_type_option_test() { + let checkbox = CheckboxTypeOption::default(); + + let mut single_select = SingleSelectTypeOption::default(); + single_select.transform_type_option(FieldType::Checkbox, checkbox.clone().into()); + debug_assert_eq!(single_select.options.len(), 2); + + // Already contain the yes/no option. It doesn't need to insert new options + single_select.transform_type_option(FieldType::Checkbox, checkbox.into()); + debug_assert_eq!(single_select.options.len(), 2); + } + + #[test] + fn single_select_transform_with_multi_select_type_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let multi_select = MultiSelectTypeOption { + options: vec![google, facebook], + disable_color: false, + }; + + let mut single_select = SingleSelectTypeOption::default(); + single_select.transform_type_option(FieldType::MultiSelect, multi_select.clone().into()); + debug_assert_eq!(single_select.options.len(), 2); + + // Already contain the yes/no option. It doesn't need to insert new options + single_select.transform_type_option(FieldType::MultiSelect, multi_select.into()); + debug_assert_eq!(single_select.options.len(), 2); + } + + #[test] + fn single_select_insert_multi_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let single_select = SingleSelectTypeOption { + options: vec![google.clone(), facebook.clone()], + disable_color: false, + }; + + let option_ids = vec![google.id.clone(), facebook.id]; + let changeset = SelectOptionCellChangeset::from_insert_options(option_ids); + let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1; + assert_eq!(&*select_option_ids, &vec![google.id]); + } + + #[test] + fn single_select_unselect_multi_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let single_select = SingleSelectTypeOption { + options: vec![google.clone(), facebook.clone()], + disable_color: false, + }; + let option_ids = vec![google.id.clone(), facebook.id]; + + // insert + let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone()); + let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1; + assert_eq!(&*select_option_ids, &vec![google.id]); + + // delete + let changeset = SelectOptionCellChangeset::from_delete_options(option_ids); + let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1; + assert!(select_option_ids.is_empty()); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs new file mode 100644 index 0000000000..bd57b749d5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs @@ -0,0 +1,64 @@ +use crate::entities::FieldType; +use crate::services::field::{ + MultiSelectTypeOption, SelectOption, SelectOptionColor, SelectOptionIds, + SelectTypeOptionSharedAction, SingleSelectTypeOption, TypeOption, CHECK, UNCHECK, +}; +use collab_database::fields::TypeOptionData; + +/// Handles how to transform the cell data when switching between different field types +pub(crate) struct SelectOptionTypeOptionTransformHelper(); +impl SelectOptionTypeOptionTransformHelper { + /// Transform the TypeOptionData from 'field_type' to single select option type. + /// + /// # Arguments + /// + /// * `old_field_type`: the FieldType of the passed-in TypeOptionData + /// + pub fn transform_type_option( + shared: &mut T, + old_field_type: &FieldType, + old_type_option_data: TypeOptionData, + ) where + T: SelectTypeOptionSharedAction + TypeOption, + { + match old_field_type { + FieldType::Checkbox => { + //add Yes and No options if it does not exist. + if !shared.options().iter().any(|option| option.name == CHECK) { + let check_option = SelectOption::with_color(CHECK, SelectOptionColor::Green); + shared.mut_options().push(check_option); + } + + if !shared.options().iter().any(|option| option.name == UNCHECK) { + let uncheck_option = SelectOption::with_color(UNCHECK, SelectOptionColor::Yellow); + shared.mut_options().push(uncheck_option); + } + }, + FieldType::MultiSelect => { + let options = MultiSelectTypeOption::from(old_type_option_data).options; + options.iter().for_each(|new_option| { + if !shared + .options() + .iter() + .any(|option| option.name == new_option.name) + { + shared.mut_options().push(new_option.clone()); + } + }) + }, + FieldType::SingleSelect => { + let options = SingleSelectTypeOption::from(old_type_option_data).options; + options.iter().for_each(|new_option| { + if !shared + .options() + .iter() + .any(|option| option.name == new_option.name) + { + shared.mut_options().push(new_option.clone()); + } + }) + }, + _ => {}, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/mod.rs new file mode 100644 index 0000000000..9537ee8f33 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/mod.rs @@ -0,0 +1,6 @@ +#![allow(clippy::module_inception)] +mod text_filter; +mod text_tests; +mod text_type_option; + +pub use text_type_option::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs new file mode 100644 index 0000000000..f684dcc56b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs @@ -0,0 +1,83 @@ +use crate::entities::{TextFilterConditionPB, TextFilterPB}; + +impl TextFilterPB { + pub fn is_visible>(&self, cell_data: T) -> bool { + let cell_data = cell_data.as_ref().to_lowercase(); + let content = &self.content.to_lowercase(); + match self.condition { + TextFilterConditionPB::Is => &cell_data == content, + TextFilterConditionPB::IsNot => &cell_data != content, + TextFilterConditionPB::Contains => cell_data.contains(content), + TextFilterConditionPB::DoesNotContain => !cell_data.contains(content), + TextFilterConditionPB::StartsWith => cell_data.starts_with(content), + TextFilterConditionPB::EndsWith => cell_data.ends_with(content), + TextFilterConditionPB::TextIsEmpty => cell_data.is_empty(), + TextFilterConditionPB::TextIsNotEmpty => !cell_data.is_empty(), + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::all)] + use crate::entities::{TextFilterConditionPB, TextFilterPB}; + + #[test] + fn text_filter_equal_test() { + let text_filter = TextFilterPB { + condition: TextFilterConditionPB::Is, + content: "appflowy".to_owned(), + }; + + assert!(text_filter.is_visible("AppFlowy")); + assert_eq!(text_filter.is_visible("appflowy"), true); + assert_eq!(text_filter.is_visible("Appflowy"), true); + assert_eq!(text_filter.is_visible("AppFlowy.io"), false); + } + #[test] + fn text_filter_start_with_test() { + let text_filter = TextFilterPB { + condition: TextFilterConditionPB::StartsWith, + content: "appflowy".to_owned(), + }; + + assert_eq!(text_filter.is_visible("AppFlowy.io"), true); + assert_eq!(text_filter.is_visible(""), false); + assert_eq!(text_filter.is_visible("https"), false); + } + + #[test] + fn text_filter_end_with_test() { + let text_filter = TextFilterPB { + condition: TextFilterConditionPB::EndsWith, + content: "appflowy".to_owned(), + }; + + assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true); + assert_eq!(text_filter.is_visible("App"), false); + assert_eq!(text_filter.is_visible("appflowy.io"), false); + } + #[test] + fn text_filter_empty_test() { + let text_filter = TextFilterPB { + condition: TextFilterConditionPB::TextIsEmpty, + content: "appflowy".to_owned(), + }; + + assert_eq!(text_filter.is_visible(""), true); + assert_eq!(text_filter.is_visible("App"), false); + } + #[test] + fn text_filter_contain_test() { + let text_filter = TextFilterPB { + condition: TextFilterConditionPB::Contains, + content: "appflowy".to_owned(), + }; + + assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true); + assert_eq!(text_filter.is_visible("AppFlowy"), true); + assert_eq!(text_filter.is_visible("App"), false); + assert_eq!(text_filter.is_visible(""), false); + assert_eq!(text_filter.is_visible("github"), false); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs new file mode 100644 index 0000000000..00337c29f0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs @@ -0,0 +1,97 @@ +#[cfg(test)] +mod tests { + use collab_database::rows::Cell; + + use crate::entities::FieldType; + use crate::services::cell::stringify_cell_data; + use crate::services::field::FieldBuilder; + use crate::services::field::*; + + // Test parser the cell data which field's type is FieldType::Date to cell data + // which field's type is FieldType::Text + #[test] + fn date_type_to_text_type() { + let field_type = FieldType::DateTime; + let field = FieldBuilder::from_field_type(field_type.clone()).build(); + + assert_eq!( + stringify_cell_data( + &to_text_cell(1647251762.to_string()), + &FieldType::RichText, + &field_type, + &field + ), + "Mar 14,2022" + ); + + let data = DateCellData { + timestamp: Some(1647251762), + include_time: true, + }; + + assert_eq!( + stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field), + "Mar 14,2022" + ); + } + + fn to_text_cell(s: String) -> Cell { + StrCellData(s).into() + } + + // Test parser the cell data which field's type is FieldType::SingleSelect to cell data + // which field's type is FieldType::Text + #[test] + fn single_select_to_text_type() { + let field_type = FieldType::SingleSelect; + let done_option = SelectOption::new("Done"); + let option_id = done_option.id.clone(); + + let single_select = SingleSelectTypeOption { + options: vec![done_option.clone()], + disable_color: false, + }; + let field = FieldBuilder::new(field_type.clone(), single_select).build(); + + assert_eq!( + stringify_cell_data( + &to_text_cell(option_id), + &FieldType::RichText, + &field_type, + &field + ), + done_option.name, + ); + } + /* + - [Unit Test] Testing the switching from Multi-selection type to Text type + - Tracking : https://github.com/AppFlowy-IO/AppFlowy/issues/1183 + */ + #[test] + fn multiselect_to_text_type() { + let field_type = FieldType::MultiSelect; + + let france = SelectOption::new("france"); + let france_option_id = france.id.clone(); + + let argentina = SelectOption::new("argentina"); + let argentina_option_id = argentina.id.clone(); + + let multi_select = MultiSelectTypeOption { + options: vec![france.clone(), argentina.clone()], + disable_color: false, + }; + + let field_rev = FieldBuilder::new(field_type.clone(), multi_select).build(); + + assert_eq!( + stringify_cell_data( + &to_text_cell(format!("{},{}", france_option_id, argentina_option_id)), + &FieldType::RichText, + &field_type, + &field_rev + ), + format!("{},{}", france.name, argentina.name) + ); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs new file mode 100644 index 0000000000..112086c6ab --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -0,0 +1,273 @@ +use crate::entities::{FieldType, TextFilterPB}; +use crate::services::cell::{ + stringify_cell_data, CellDataChangeset, CellDataDecoder, CellProtobufBlobParser, DecodedCellData, + FromCellString, +}; +use crate::services::field::{ + TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionTransform, CELL_DATE, +}; +use bytes::Bytes; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; + +use crate::services::field::type_options::util::ProtobufStr; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use flowy_error::{FlowyError, FlowyResult}; + +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +/// For the moment, the `RichTextTypeOptionPB` is empty. The `data` property is not +/// used yet. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RichTextTypeOption { + #[serde(default)] + pub inner: String, +} + +impl TypeOption for RichTextTypeOption { + type CellData = StrCellData; + type CellChangeset = String; + type CellProtobufType = ProtobufStr; + type CellFilter = TextFilterPB; +} + +impl From for RichTextTypeOption { + fn from(data: TypeOptionData) -> Self { + let s = data.get_str_value(CELL_DATE).unwrap_or_default(); + Self { inner: s } + } +} + +impl From for TypeOptionData { + fn from(data: RichTextTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_str_value(CELL_DATE, data.inner) + .build() + } +} + +impl TypeOptionTransform for RichTextTypeOption { + fn transformable(&self) -> bool { + true + } + + fn transform_type_option( + &mut self, + _old_type_option_field_type: FieldType, + _old_type_option_data: TypeOptionData, + ) { + } + + fn transform_type_option_cell( + &self, + cell: &Cell, + _decoded_field_type: &FieldType, + _field: &Field, + ) -> Option<::CellData> { + if _decoded_field_type.is_date() + || _decoded_field_type.is_single_select() + || _decoded_field_type.is_multi_select() + || _decoded_field_type.is_number() + || _decoded_field_type.is_url() + { + Some(StrCellData::from(stringify_cell_data( + cell, + _decoded_field_type, + _decoded_field_type, + _field, + ))) + } else { + Some(StrCellData::from(cell)) + } + } +} + +impl TypeOptionCellData for RichTextTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + ProtobufStr::from(cell_data.0) + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(StrCellData::from(cell)) + } +} + +impl CellDataDecoder for RichTextTypeOption { + fn decode_cell_str( + &self, + cell: &Cell, + _decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + Ok(StrCellData::from(cell)) + } + + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String { + cell_data.to_string() + } + + fn decode_cell_to_str(&self, cell: &Cell) -> String { + Self::CellData::from(cell).to_string() + } +} + +impl CellDataChangeset for RichTextTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + if changeset.len() > 10000 { + Err(FlowyError::text_too_long().context("The len of the text should not be more than 10000")) + } else { + let text_cell_data = StrCellData(changeset); + Ok((text_cell_data.clone().into(), text_cell_data)) + } + } +} + +impl TypeOptionCellDataFilter for RichTextTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_text() { + return false; + } + + filter.is_visible(cell_data) + } +} + +impl TypeOptionCellDataCompare for RichTextTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + cell_data.0.cmp(&other_cell_data.0) + } +} + +#[derive(Clone)] +pub struct TextCellData(pub String); +impl AsRef for TextCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::ops::Deref for TextCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromCellString for TextCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Ok(TextCellData(s.to_owned())) + } +} + +impl ToString for TextCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl DecodedCellData for TextCellData { + type Object = TextCellData; + + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +pub struct TextCellDataParser(); +impl CellProtobufBlobParser for TextCellDataParser { + type Object = TextCellData; + fn parser(bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => Ok(TextCellData(s)), + Err(_) => Ok(TextCellData("".to_owned())), + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct StrCellData(pub String); +impl std::ops::Deref for StrCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&Cell> for StrCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_str_value("data").unwrap_or_default()) + } +} + +impl From for Cell { + fn from(data: StrCellData) -> Self { + new_cell_builder(FieldType::RichText) + .insert_str_value("data", data.0) + .build() + } +} + +impl std::ops::DerefMut for StrCellData { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromCellString for StrCellData { + fn from_cell_str(s: &str) -> FlowyResult { + Ok(Self(s.to_owned())) + } +} + +impl std::convert::From for StrCellData { + fn from(s: String) -> Self { + Self(s) + } +} + +impl ToString for StrCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl std::convert::From for String { + fn from(value: StrCellData) -> Self { + value.0 + } +} + +impl std::convert::From<&str> for StrCellData { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl AsRef for StrCellData { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs new file mode 100644 index 0000000000..b69920d740 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -0,0 +1,230 @@ +use std::cmp::Ordering; +use std::fmt::Debug; + +use bytes::Bytes; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::Cell; +use protobuf::ProtobufError; + +use flowy_error::FlowyResult; + +use crate::entities::{ + CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, + MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, + URLTypeOptionPB, +}; +use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset}; +use crate::services::field::{ + CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, + RichTextTypeOption, SingleSelectTypeOption, URLTypeOption, +}; +use crate::services::filter::FromFilterString; + +pub trait TypeOption { + /// `CellData` represents as the decoded model for current type option. Each of them impl the + /// `FromCellString` and `Default` trait. If the cell string can not be decoded into the specified + /// cell data type then the default value will be returned. + /// For example: + /// FieldType::Checkbox => CheckboxCellData + /// FieldType::Date => DateCellData + /// FieldType::URL => URLCellData + /// + /// Uses `StrCellData` for any `TypeOption` if their cell data is pure `String`. + /// + type CellData: ToString + Default + Send + Sync + Clone + Debug + 'static; + + /// Represents as the corresponding field type cell changeset. + /// The changeset must implements the `FromCellChangesetString` and the `ToCellChangesetString` trait. + /// These two traits are auto implemented for `String`. + /// + type CellChangeset: FromCellChangeset + ToCellChangeset; + + /// For the moment, the protobuf type only be used in the FFI of `Dart`. If the decoded cell + /// struct is just a `String`, then use the `StrCellData` as its `CellProtobufType`. + /// Otherwise, providing a custom protobuf type as its `CellProtobufType`. + /// For example: + /// FieldType::Date => DateCellDataPB + /// FieldType::URL => URLCellDataPB + /// + type CellProtobufType: TryInto + Debug; + + /// Represents as the filter configuration for this type option. + type CellFilter: FromFilterString + Send + Sync + 'static; +} + +pub trait TypeOptionCellData: TypeOption { + /// Convert the decoded cell data into corresponding `Protobuf struct`. + /// For example: + /// FieldType::URL => URLCellDataPB + /// FieldType::Date=> DateCellDataPB + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType; + + /// Decodes the opaque cell string to corresponding data struct. + // For example, the cell data is timestamp if its field type is `FieldType::Date`. This cell + // data can not directly show to user. So it needs to be encode as the date string with custom + // format setting. Encode `1647251762` to `"Mar 14,2022` + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData>; +} + +pub trait TypeOptionTransform: TypeOption { + /// Returns true if the current `TypeOption` provides custom type option transformation + fn transformable(&self) -> bool { + false + } + + /// Transform the TypeOption from one field type to another + /// For example, when switching from `checkbox` type-option to `single-select` + /// type-option, adding the `Yes` option if the `single-select` type-option doesn't contain it. + /// But the cell content is a string, `Yes`, it's need to do the cell content transform. + /// The `Yes` string will be transformed to the `Yes` option id. + /// + /// # Arguments + /// + /// * `old_type_option_field_type`: the FieldType of the passed-in TypeOption + /// * `old_type_option_data`: the data that can be parsed into corresponding `TypeOption`. + /// + /// + fn transform_type_option( + &mut self, + _old_type_option_field_type: FieldType, + _old_type_option_data: TypeOptionData, + ) { + } + + /// Transform the cell data from one field type to another + /// + /// # Arguments + /// + /// * `cell_str`: the cell string of the current field type + /// * `decoded_field_type`: the field type of the cell data that's going to be transformed into + /// current `TypeOption` field type. + /// + fn transform_type_option_cell( + &self, + _cell: &Cell, + _decoded_field_type: &FieldType, + _field: &Field, + ) -> Option<::CellData> { + None + } +} + +pub trait TypeOptionCellDataFilter: TypeOption + CellDataDecoder { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool; +} + +#[inline(always)] +pub fn default_order() -> Ordering { + Ordering::Equal +} + +pub trait TypeOptionCellDataCompare: TypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering; +} + +pub fn type_option_data_from_pb_or_default>( + bytes: T, + field_type: &FieldType, +) -> TypeOptionData { + let bytes = bytes.into(); + let result: Result = match field_type { + FieldType::RichText => { + RichTextTypeOptionPB::try_from(bytes).map(|pb| RichTextTypeOption::from(pb).into()) + }, + FieldType::Number => { + NumberTypeOptionPB::try_from(bytes).map(|pb| NumberTypeOption::from(pb).into()) + }, + FieldType::DateTime => { + DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into()) + }, + FieldType::SingleSelect => { + SingleSelectTypeOptionPB::try_from(bytes).map(|pb| SingleSelectTypeOption::from(pb).into()) + }, + FieldType::MultiSelect => { + MultiSelectTypeOptionPB::try_from(bytes).map(|pb| MultiSelectTypeOption::from(pb).into()) + }, + FieldType::Checkbox => { + CheckboxTypeOptionPB::try_from(bytes).map(|pb| CheckboxTypeOption::from(pb).into()) + }, + FieldType::URL => URLTypeOptionPB::try_from(bytes).map(|pb| URLTypeOption::from(pb).into()), + FieldType::Checklist => { + ChecklistTypeOptionPB::try_from(bytes).map(|pb| ChecklistTypeOption::from(pb).into()) + }, + }; + + result.unwrap_or_else(|_| default_type_option_data_for_type(field_type)) +} + +pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> Bytes { + match field_type { + FieldType::RichText => { + let rich_text_type_option: RichTextTypeOption = type_option.into(); + RichTextTypeOptionPB::from(rich_text_type_option) + .try_into() + .unwrap() + }, + FieldType::Number => { + let number_type_option: NumberTypeOption = type_option.into(); + NumberTypeOptionPB::from(number_type_option) + .try_into() + .unwrap() + }, + FieldType::DateTime => { + let date_type_option: DateTypeOption = type_option.into(); + DateTypeOptionPB::from(date_type_option).try_into().unwrap() + }, + FieldType::SingleSelect => { + let single_select_type_option: SingleSelectTypeOption = type_option.into(); + SingleSelectTypeOptionPB::from(single_select_type_option) + .try_into() + .unwrap() + }, + FieldType::MultiSelect => { + let multi_select_type_option: MultiSelectTypeOption = type_option.into(); + MultiSelectTypeOptionPB::from(multi_select_type_option) + .try_into() + .unwrap() + }, + FieldType::Checkbox => { + let checkbox_type_option: CheckboxTypeOption = type_option.into(); + CheckboxTypeOptionPB::from(checkbox_type_option) + .try_into() + .unwrap() + }, + FieldType::URL => { + let url_type_option: URLTypeOption = type_option.into(); + URLTypeOptionPB::from(url_type_option).try_into().unwrap() + }, + FieldType::Checklist => { + let checklist_type_option: ChecklistTypeOption = type_option.into(); + ChecklistTypeOptionPB::from(checklist_type_option) + .try_into() + .unwrap() + }, + } +} + +pub fn default_type_option_data_for_type(field_type: &FieldType) -> TypeOptionData { + match field_type { + FieldType::RichText => RichTextTypeOption::default().into(), + FieldType::Number => NumberTypeOption::default().into(), + FieldType::DateTime => DateTypeOption::default().into(), + FieldType::SingleSelect => SingleSelectTypeOption::default().into(), + FieldType::MultiSelect => MultiSelectTypeOption::default().into(), + FieldType::Checkbox => CheckboxTypeOption::default().into(), + FieldType::URL => URLTypeOption::default().into(), + FieldType::Checklist => ChecklistTypeOption::default().into(), + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs new file mode 100644 index 0000000000..f528d7ff8a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -0,0 +1,570 @@ +use std::any::Any; +use std::cmp::Ordering; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{Cell, RowId}; + +use flowy_error::FlowyResult; + +use crate::entities::FieldType; +use crate::services::cell::{ + CellCache, CellDataChangeset, CellDataDecoder, CellFilterCache, CellProtobufBlob, + FromCellChangeset, +}; +use crate::services::field::{ + CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, + RichTextTypeOption, SingleSelectTypeOption, TypeOption, TypeOptionCellData, + TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOption, +}; + +pub const CELL_DATE: &str = "data"; + +/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait +/// Only object-safe traits can be made into trait objects. +/// > Object-safe traits are traits with methods that follow these two rules: +/// 1.the return type is not Self. +/// 2.there are no generic types parameters. +/// +pub trait TypeOptionCellDataHandler: Send + Sync + 'static { + fn handle_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field_rev: &Field, + ) -> FlowyResult; + + fn handle_cell_changeset( + &self, + cell_changeset: String, + old_cell: Option, + field: &Field, + ) -> FlowyResult; + + fn handle_cell_compare(&self, left_cell: &Cell, right_cell: &Cell, field: &Field) -> Ordering; + + fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool; + + /// Decode the cell_str to corresponding cell data, and then return the display string of the + /// cell data. + fn stringify_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field: &Field, + ) -> String; + + fn get_cell_data( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field: &Field, + ) -> FlowyResult; +} + +struct CellDataCacheKey(u64); +impl CellDataCacheKey { + pub fn new(field_rev: &Field, decoded_field_type: FieldType, cell: &Cell) -> Self { + let mut hasher = DefaultHasher::new(); + if let Some(type_option_data) = field_rev.get_any_type_option(&decoded_field_type) { + type_option_data.hash(&mut hasher); + } + hasher.write(field_rev.id.as_bytes()); + hasher.write_u8(decoded_field_type as u8); + cell.hash(&mut hasher); + Self(hasher.finish()) + } +} + +impl AsRef for CellDataCacheKey { + fn as_ref(&self) -> &u64 { + &self.0 + } +} + +struct TypeOptionCellDataHandlerImpl { + inner: T, + cell_data_cache: Option, + cell_filter_cache: Option, +} + +impl TypeOptionCellDataHandlerImpl +where + T: TypeOption + + CellDataDecoder + + CellDataChangeset + + TypeOptionCellData + + TypeOptionTransform + + TypeOptionCellDataFilter + + TypeOptionCellDataCompare + + Send + + Sync + + 'static, +{ + pub fn new_with_boxed( + inner: T, + cell_filter_cache: Option, + cell_data_cache: Option, + ) -> Box { + Box::new(Self { + inner, + cell_data_cache, + cell_filter_cache, + }) as Box + } +} + +impl TypeOptionCellDataHandlerImpl +where + T: TypeOption + CellDataDecoder + Send + Sync, +{ + fn get_decoded_cell_data( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field: &Field, + ) -> FlowyResult<::CellData> { + let key = CellDataCacheKey::new(field, decoded_field_type.clone(), cell); + if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { + let read_guard = cell_data_cache.read(); + if let Some(cell_data) = read_guard.get(key.as_ref()).cloned() { + tracing::trace!( + "Cell cache hit: field_type:{}, cell: {:?}, cell_data: {:?}", + decoded_field_type, + cell, + cell_data + ); + return Ok(cell_data); + } + } + + let cell_data = self.decode_cell_str(cell, decoded_field_type, field)?; + if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { + tracing::trace!( + "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}", + decoded_field_type, + cell, + cell_data + ); + cell_data_cache + .write() + .insert(key.as_ref(), cell_data.clone()); + } + Ok(cell_data) + } + + fn set_decoded_cell_data( + &self, + cell: &Cell, + cell_data: ::CellData, + field: &Field, + ) { + if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { + let field_type = FieldType::from(field.field_type); + let key = CellDataCacheKey::new(field, field_type.clone(), cell); + tracing::trace!( + "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}", + field_type, + cell, + cell_data + ); + cell_data_cache.write().insert(key.as_ref(), cell_data); + } + } +} + +impl std::ops::Deref for TypeOptionCellDataHandlerImpl { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl TypeOption for TypeOptionCellDataHandlerImpl +where + T: TypeOption + Send + Sync, +{ + type CellData = T::CellData; + type CellChangeset = T::CellChangeset; + type CellProtobufType = T::CellProtobufType; + type CellFilter = T::CellFilter; +} + +impl TypeOptionCellDataHandler for TypeOptionCellDataHandlerImpl +where + T: TypeOption + + CellDataDecoder + + CellDataChangeset + + TypeOptionCellData + + TypeOptionTransform + + TypeOptionCellDataFilter + + TypeOptionCellDataCompare + + Send + + Sync + + 'static, +{ + fn handle_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field_rev: &Field, + ) -> FlowyResult { + let cell_data = self + .get_cell_data(cell, decoded_field_type, field_rev)? + .unbox_or_default::<::CellData>(); + + CellProtobufBlob::from(self.convert_to_protobuf(cell_data)) + } + + fn handle_cell_changeset( + &self, + cell_changeset: String, + old_cell: Option, + field: &Field, + ) -> FlowyResult { + let changeset = ::CellChangeset::from_changeset(cell_changeset)?; + let (cell, cell_data) = self.apply_changeset(changeset, old_cell)?; + self.set_decoded_cell_data(&cell, cell_data, field); + Ok(cell) + } + + fn handle_cell_compare(&self, left_cell: &Cell, right_cell: &Cell, field: &Field) -> Ordering { + let field_type = FieldType::from(field.field_type); + let left = self + .get_decoded_cell_data(left_cell, &field_type, field) + .unwrap_or_default(); + let right = self + .get_decoded_cell_data(right_cell, &field_type, field) + .unwrap_or_default(); + self.apply_cmp(&left, &right) + } + + fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool { + let perform_filter = || { + let filter_cache = self.cell_filter_cache.as_ref()?.read(); + let cell_filter = filter_cache.get::<::CellFilter>(&field.id)?; + let cell_data = self.get_decoded_cell_data(cell, field_type, field).ok()?; + Some(self.apply_filter(cell_filter, field_type, &cell_data)) + }; + + perform_filter().unwrap_or(true) + } + + fn stringify_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field: &Field, + ) -> String { + if self.transformable() { + let cell_data = self.transform_type_option_cell(cell, decoded_field_type, field); + if let Some(cell_data) = cell_data { + return self.decode_cell_data_to_str(cell_data); + } + } + self.decode_cell_to_str(cell) + } + + fn get_cell_data( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + field: &Field, + ) -> FlowyResult { + // tracing::debug!("get_cell_data: {:?}", std::any::type_name::()); + let cell_data = if self.transformable() { + match self.transform_type_option_cell(cell, decoded_field_type, field) { + None => self.get_decoded_cell_data(cell, decoded_field_type, field)?, + Some(cell_data) => cell_data, + } + } else { + self.get_decoded_cell_data(cell, decoded_field_type, field)? + }; + Ok(BoxCellData::new(cell_data)) + } +} + +pub struct TypeOptionCellExt<'a> { + field: &'a Field, + cell_data_cache: Option, + cell_filter_cache: Option, +} + +impl<'a> TypeOptionCellExt<'a> { + pub fn new_with_cell_data_cache(field: &'a Field, cell_data_cache: Option) -> Self { + Self { + field, + cell_data_cache, + cell_filter_cache: None, + } + } + + pub fn new( + field: &'a Field, + cell_data_cache: Option, + cell_filter_cache: Option, + ) -> Self { + let mut this = Self::new_with_cell_data_cache(field, cell_data_cache); + this.cell_filter_cache = cell_filter_cache; + this + } + + pub fn get_cells(&self) -> Vec { + let field_type = FieldType::from(self.field.field_type); + match self.get_type_option_cell_data_handler(&field_type) { + None => vec![], + Some(_handler) => { + todo!() + }, + } + } + + pub fn get_type_option_cell_data_handler( + &self, + field_type: &FieldType, + ) -> Option> { + match field_type { + FieldType::RichText => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + FieldType::Number => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + FieldType::DateTime => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + FieldType::SingleSelect => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + FieldType::MultiSelect => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + FieldType::Checkbox => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + FieldType::URL => { + self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }) + }, + FieldType::Checklist => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), + } + } +} + +pub fn transform_type_option( + type_option_data: &TypeOptionData, + new_field_type: &FieldType, + old_type_option_data: Option, + old_field_type: FieldType, +) -> TypeOptionData { + let mut transform_handler = get_type_option_transform_handler(type_option_data, new_field_type); + if let Some(old_type_option_data) = old_type_option_data { + transform_handler.transform(old_field_type, old_type_option_data); + } + transform_handler.to_type_option_data() +} + +/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait. +pub trait TypeOptionTransformHandler { + fn transform( + &mut self, + old_type_option_field_type: FieldType, + old_type_option_data: TypeOptionData, + ); + + fn to_type_option_data(&self) -> TypeOptionData; +} + +impl TypeOptionTransformHandler for T +where + T: TypeOptionTransform + Into + Clone, +{ + fn transform( + &mut self, + old_type_option_field_type: FieldType, + old_type_option_data: TypeOptionData, + ) { + if self.transformable() { + self.transform_type_option(old_type_option_field_type, old_type_option_data) + } + } + + fn to_type_option_data(&self) -> TypeOptionData { + self.clone().into() + } +} +fn get_type_option_transform_handler( + type_option_data: &TypeOptionData, + field_type: &FieldType, +) -> Box { + let type_option_data = type_option_data.clone(); + match field_type { + FieldType::RichText => { + Box::new(RichTextTypeOption::from(type_option_data)) as Box + }, + FieldType::Number => { + Box::new(NumberTypeOption::from(type_option_data)) as Box + }, + FieldType::DateTime => { + Box::new(DateTypeOption::from(type_option_data)) as Box + }, + FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data)) + as Box, + FieldType::MultiSelect => { + Box::new(MultiSelectTypeOption::from(type_option_data)) as Box + }, + FieldType::Checkbox => { + Box::new(CheckboxTypeOption::from(type_option_data)) as Box + }, + FieldType::URL => { + Box::new(URLTypeOption::from(type_option_data)) as Box + }, + FieldType::Checklist => { + Box::new(ChecklistTypeOption::from(type_option_data)) as Box + }, + } +} + +pub struct BoxCellData(Box); + +impl BoxCellData { + fn new(value: T) -> Self + where + T: Send + Sync + 'static, + { + Self(Box::new(value)) + } + + fn unbox_or_default(self) -> T + where + T: Default + 'static, + { + match self.0.downcast::() { + Ok(value) => *value, + Err(_) => T::default(), + } + } + + pub(crate) fn unbox_or_none(self) -> Option + where + T: Default + 'static, + { + match self.0.downcast::() { + Ok(value) => Some(*value), + Err(_) => None, + } + } + + #[allow(dead_code)] + fn downcast_ref(&self) -> Option<&T> { + self.0.downcast_ref() + } +} + +pub struct RowSingleCellData { + pub row_id: RowId, + pub field_id: String, + pub field_type: FieldType, + pub cell_data: BoxCellData, +} + +macro_rules! into_cell_data { + ($func_name:ident,$return_ty:ty) => { + #[allow(dead_code)] + pub fn $func_name(self) -> Option<$return_ty> { + self.cell_data.unbox_or_none() + } + }; +} + +impl RowSingleCellData { + into_cell_data!( + into_text_field_cell_data, + ::CellData + ); + into_cell_data!( + into_number_field_cell_data, + ::CellData + ); + into_cell_data!( + into_url_field_cell_data, + ::CellData + ); + into_cell_data!( + into_single_select_field_cell_data, + ::CellData + ); + into_cell_data!( + into_multi_select_field_cell_data, + ::CellData + ); + into_cell_data!( + into_date_field_cell_data, + ::CellData + ); + into_cell_data!( + into_check_list_field_cell_data, + ::CellData + ); +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/mod.rs new file mode 100644 index 0000000000..8f6cb884df --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod url_tests; +mod url_type_option; +mod url_type_option_entities; + +pub use url_type_option::*; +pub use url_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs new file mode 100644 index 0000000000..99a8bbf44a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs @@ -0,0 +1,167 @@ +#[cfg(test)] +mod tests { + use collab_database::fields::Field; + + use crate::entities::FieldType; + use crate::services::cell::CellDataChangeset; + use crate::services::field::FieldBuilder; + use crate::services::field::URLTypeOption; + + /// The expected_str will equal to the input string, but the expected_url will be empty if there's no + /// http url in the input string. + #[test] + fn url_type_option_does_not_contain_url_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field = FieldBuilder::from_field_type(field_type).build(); + assert_url(&type_option, "123", "123", "", &field); + assert_url(&type_option, "", "", "", &field); + } + + /// The expected_str will equal to the input string, but the expected_url will not be empty + /// if there's a http url in the input string. + #[test] + fn url_type_option_contains_url_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field = FieldBuilder::from_field_type(field_type).build(); + assert_url( + &type_option, + "AppFlowy website - https://www.appflowy.io", + "AppFlowy website - https://www.appflowy.io", + "https://www.appflowy.io/", + &field, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io", + "AppFlowy website appflowy.io", + "https://appflowy.io", + &field, + ); + } + + /// if there's a http url and some words following it in the input string. + #[test] + fn url_type_option_contains_url_with_string_after_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field = FieldBuilder::from_field_type(field_type).build(); + assert_url( + &type_option, + "AppFlowy website - https://www.appflowy.io welcome!", + "AppFlowy website - https://www.appflowy.io welcome!", + "https://www.appflowy.io/", + &field, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io welcome!", + "AppFlowy website appflowy.io welcome!", + "https://appflowy.io", + &field, + ); + } + + /// if there's a http url and special words following it in the input string. + #[test] + fn url_type_option_contains_url_with_special_string_after_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field = FieldBuilder::from_field_type(field_type).build(); + assert_url( + &type_option, + "AppFlowy website - https://www.appflowy.io!", + "AppFlowy website - https://www.appflowy.io!", + "https://www.appflowy.io/", + &field, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io!", + "AppFlowy website appflowy.io!", + "https://appflowy.io", + &field, + ); + } + + /// if there's a level4 url in the input string. + #[test] + fn level4_url_type_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field = FieldBuilder::from_field_type(field_type).build(); + assert_url( + &type_option, + "test - https://tester.testgroup.appflowy.io", + "test - https://tester.testgroup.appflowy.io", + "https://tester.testgroup.appflowy.io/", + &field, + ); + + assert_url( + &type_option, + "test tester.testgroup.appflowy.io", + "test tester.testgroup.appflowy.io", + "https://tester.testgroup.appflowy.io", + &field, + ); + } + + /// urls with different top level domains. + #[test] + fn different_top_level_domains_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field = FieldBuilder::from_field_type(field_type).build(); + assert_url( + &type_option, + "appflowy - https://appflowy.com", + "appflowy - https://appflowy.com", + "https://appflowy.com/", + &field, + ); + + assert_url( + &type_option, + "appflowy - https://appflowy.top", + "appflowy - https://appflowy.top", + "https://appflowy.top/", + &field, + ); + + assert_url( + &type_option, + "appflowy - https://appflowy.net", + "appflowy - https://appflowy.net", + "https://appflowy.net/", + &field, + ); + + assert_url( + &type_option, + "appflowy - https://appflowy.edu", + "appflowy - https://appflowy.edu", + "https://appflowy.edu/", + &field, + ); + } + + fn assert_url( + type_option: &URLTypeOption, + input_str: &str, + expected_str: &str, + expected_url: &str, + _field: &Field, + ) { + let decode_cell_data = type_option + .apply_changeset(input_str.to_owned(), None) + .unwrap() + .1; + assert_eq!(expected_str.to_owned(), decode_cell_data.data); + assert_eq!(expected_url.to_owned(), decode_cell_data.url); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs new file mode 100644 index 0000000000..69721a80db --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs @@ -0,0 +1,151 @@ +use crate::entities::{FieldType, TextFilterPB, URLCellDataPB}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::{ + TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionTransform, URLCellData, +}; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use fancy_regex::Regex; +use flowy_error::FlowyResult; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct URLTypeOption { + pub url: String, + pub content: String, +} + +impl TypeOption for URLTypeOption { + type CellData = URLCellData; + type CellChangeset = URLCellChangeset; + type CellProtobufType = URLCellDataPB; + type CellFilter = TextFilterPB; +} + +impl From for URLTypeOption { + fn from(data: TypeOptionData) -> Self { + let url = data.get_str_value("url").unwrap_or_default(); + let content = data.get_str_value("content").unwrap_or_default(); + Self { url, content } + } +} + +impl From for TypeOptionData { + fn from(data: URLTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_str_value("url", data.url) + .insert_str_value("content", data.content) + .build() + } +} + +impl TypeOptionTransform for URLTypeOption {} + +impl TypeOptionCellData for URLTypeOption { + fn convert_to_protobuf( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + cell_data.into() + } + + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(URLCellData::from(cell)) + } +} + +impl CellDataDecoder for URLTypeOption { + fn decode_cell_str( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + if !decoded_field_type.is_url() { + return Ok(Default::default()); + } + + self.decode_cell(cell) + } + + fn decode_cell_data_to_str(&self, cell_data: ::CellData) -> String { + cell_data.data + } + + fn decode_cell_to_str(&self, cell: &Cell) -> String { + let cell_data = Self::CellData::from(cell); + self.decode_cell_data_to_str(cell_data) + } +} + +pub type URLCellChangeset = String; + +impl CellDataChangeset for URLTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let mut url = "".to_string(); + if let Ok(Some(m)) = URL_REGEX.find(&changeset) { + url = auto_append_scheme(m.as_str()); + } + let url_cell_data = URLCellData { + url, + data: changeset, + }; + Ok((url_cell_data.clone().into(), url_cell_data)) + } +} + +impl TypeOptionCellDataFilter for URLTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_url() { + return true; + } + + filter.is_visible(cell_data) + } +} + +impl TypeOptionCellDataCompare for URLTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + ) -> Ordering { + cell_data.data.cmp(&other_cell_data.data) + } +} +fn auto_append_scheme(s: &str) -> String { + // Only support https scheme by now + match url::Url::parse(s) { + Ok(url) => { + if url.scheme() == "https" { + url.into() + } else { + format!("https://{}", s) + } + }, + Err(_) => { + format!("https://{}", s) + }, + } +} + +lazy_static! { + static ref URL_REGEX: Regex = Regex::new( + "[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" + ) + .unwrap(); +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs new file mode 100644 index 0000000000..c797539dae --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -0,0 +1,105 @@ +use crate::entities::{FieldType, URLCellDataPB}; +use crate::services::cell::{CellProtobufBlobParser, DecodedCellData, FromCellString}; +use crate::services::field::CELL_DATE; +use bytes::Bytes; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use flowy_error::{internal_error, FlowyResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct URLCellData { + pub url: String, + pub data: String, +} + +impl URLCellData { + pub fn new(s: &str) -> Self { + Self { + url: "".to_string(), + data: s.to_string(), + } + } + + pub fn to_json(&self) -> FlowyResult { + serde_json::to_string(self).map_err(internal_error) + } +} + +impl From<&Cell> for URLCellData { + fn from(cell: &Cell) -> Self { + let url = cell.get_str_value("url").unwrap_or_default(); + let content = cell.get_str_value(CELL_DATE).unwrap_or_default(); + Self { url, data: content } + } +} + +impl From for Cell { + fn from(data: URLCellData) -> Self { + new_cell_builder(FieldType::URL) + .insert_str_value("url", data.url) + .insert_str_value(CELL_DATE, data.data) + .build() + } +} + +impl From for URLCellDataPB { + fn from(data: URLCellData) -> Self { + Self { + url: data.url, + content: data.data, + } + } +} + +impl DecodedCellData for URLCellDataPB { + type Object = URLCellDataPB; + + fn is_empty(&self) -> bool { + self.content.is_empty() + } +} + +impl From for URLCellData { + fn from(data: URLCellDataPB) -> Self { + Self { + url: data.url, + data: data.content, + } + } +} + +impl AsRef for URLCellData { + fn as_ref(&self) -> &str { + &self.url + } +} + +impl DecodedCellData for URLCellData { + type Object = URLCellData; + + fn is_empty(&self) -> bool { + self.data.is_empty() + } +} + +pub struct URLCellDataParser(); +impl CellProtobufBlobParser for URLCellDataParser { + type Object = URLCellDataPB; + + fn parser(bytes: &Bytes) -> FlowyResult { + URLCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} + +impl FromCellString for URLCellData { + fn from_cell_str(s: &str) -> FlowyResult { + serde_json::from_str::(s).map_err(internal_error) + } +} + +impl ToString for URLCellData { + fn to_string(&self) -> String { + self.to_json().unwrap() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs new file mode 100644 index 0000000000..6bf03b127a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs @@ -0,0 +1,49 @@ +use bytes::Bytes; +use protobuf::ProtobufError; + +#[derive(Default, Debug, Clone)] +pub struct ProtobufStr(pub String); +impl std::ops::Deref for ProtobufStr { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for ProtobufStr { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::convert::From for ProtobufStr { + fn from(s: String) -> Self { + Self(s) + } +} + +impl ToString for ProtobufStr { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl std::convert::TryFrom for Bytes { + type Error = ProtobufError; + + fn try_from(value: ProtobufStr) -> Result { + Ok(Bytes::from(value.0)) + } +} + +impl AsRef<[u8]> for ProtobufStr { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl AsRef for ProtobufStr { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs new file mode 100644 index 0000000000..a6ab492b04 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -0,0 +1,443 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::{Cell, Row, RowId}; +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use flowy_error::FlowyResult; +use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; +use lib_infra::future::Fut; + +use crate::entities::filter_entities::*; +use crate::entities::{FieldType, InsertedRowPB, RowPB}; +use crate::services::cell::{AnyTypeCache, CellCache, CellFilterCache}; +use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; +use crate::services::field::*; +use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification}; + +pub trait FilterDelegate: Send + Sync + 'static { + fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>>; + fn get_field(&self, field_id: &str) -> Fut>>; + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; + fn get_rows(&self, view_id: &str) -> Fut>>; + fn get_row(&self, view_id: &str, rows_id: RowId) -> Fut)>>; +} + +pub trait FromFilterString { + fn from_filter(filter: &Filter) -> Self + where + Self: Sized; +} + +pub struct FilterController { + view_id: String, + handler_id: String, + delegate: Box, + result_by_row_id: DashMap, + cell_cache: CellCache, + cell_filter_cache: CellFilterCache, + task_scheduler: Arc>, + notifier: DatabaseViewChangedNotifier, +} + +impl Drop for FilterController { + fn drop(&mut self) { + tracing::trace!("Drop {}", std::any::type_name::()); + } +} + +impl FilterController { + pub async fn new( + view_id: &str, + handler_id: &str, + delegate: T, + task_scheduler: Arc>, + filters: Vec>, + cell_cache: CellCache, + notifier: DatabaseViewChangedNotifier, + ) -> Self + where + T: FilterDelegate + 'static, + { + let this = Self { + view_id: view_id.to_string(), + handler_id: handler_id.to_string(), + delegate: Box::new(delegate), + result_by_row_id: DashMap::default(), + cell_cache, + // Cache by field_id + cell_filter_cache: AnyTypeCache::::new(), + task_scheduler, + notifier, + }; + this.refresh_filters(filters).await; + this + } + + pub async fn close(&self) { + if let Ok(mut task_scheduler) = self.task_scheduler.try_write() { + task_scheduler.unregister_handler(&self.handler_id).await; + } else { + tracing::error!("Try to get the lock of task_scheduler failed"); + } + } + + #[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))] + async fn gen_task(&self, task_type: FilterEvent, qos: QualityOfService) { + let task_id = self.task_scheduler.read().await.next_task_id(); + let task = Task::new( + &self.handler_id, + task_id, + TaskContent::Text(task_type.to_string()), + qos, + ); + self.task_scheduler.write().await.add_task(task); + } + + pub async fn filter_rows(&self, rows: &mut Vec>) { + if self.cell_filter_cache.read().is_empty() { + return; + } + let field_by_field_id = self.get_field_map().await; + rows.iter().for_each(|row| { + let _ = filter_row( + row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &self.cell_filter_cache, + ); + }); + + rows.retain(|row| { + self + .result_by_row_id + .get(&row.id) + .map(|result| result.is_visible()) + .unwrap_or(false) + }); + } + + async fn get_field_map(&self) -> HashMap> { + self + .delegate + .get_fields(&self.view_id, None) + .await + .into_iter() + .map(|field| (field.id.clone(), field)) + .collect::>>() + } + + #[tracing::instrument( + name = "process_filter_task", + level = "trace", + skip_all, + fields(filter_result), + err + )] + pub async fn process(&self, predicate: &str) -> FlowyResult<()> { + let event_type = FilterEvent::from_str(predicate).unwrap(); + match event_type { + FilterEvent::FilterDidChanged => self.filter_all_rows().await?, + FilterEvent::RowDidChanged(row_id) => self.filter_row(row_id).await?, + } + Ok(()) + } + + async fn filter_row(&self, row_id: RowId) -> FlowyResult<()> { + if let Some((_, row)) = self.delegate.get_row(&self.view_id, row_id).await { + let field_by_field_id = self.get_field_map().await; + let mut notification = FilterResultNotification::new(self.view_id.clone()); + if let Some((row_id, is_visible)) = filter_row( + &row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &self.cell_filter_cache, + ) { + if is_visible { + if let Some((index, row)) = self.delegate.get_row(&self.view_id, row_id).await { + let row_pb = RowPB::from(row.as_ref()); + notification + .visible_rows + .push(InsertedRowPB::with_index(row_pb, index as i32)) + } + } else { + notification.invisible_rows.push(row_id.into()); + } + } + + let _ = self + .notifier + .send(DatabaseViewChanged::FilterNotification(notification)); + } + Ok(()) + } + + async fn filter_all_rows(&self) -> FlowyResult<()> { + let field_by_field_id = self.get_field_map().await; + let mut visible_rows = vec![]; + let mut invisible_rows = vec![]; + + for (index, row) in self + .delegate + .get_rows(&self.view_id) + .await + .into_iter() + .enumerate() + { + if let Some((row_id, is_visible)) = filter_row( + &row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &self.cell_filter_cache, + ) { + if is_visible { + let row_pb = RowPB::from(row.as_ref()); + visible_rows.push(InsertedRowPB::with_index(row_pb, index as i32)) + } else { + invisible_rows.push(i64::from(row_id)); + } + } + } + + let notification = FilterResultNotification { + view_id: self.view_id.clone(), + invisible_rows, + visible_rows, + }; + tracing::Span::current().record("filter_result", format!("{:?}", ¬ification).as_str()); + let _ = self + .notifier + .send(DatabaseViewChanged::FilterNotification(notification)); + Ok(()) + } + + pub async fn did_receive_row_changed(&self, row_id: RowId) { + self + .gen_task( + FilterEvent::RowDidChanged(row_id), + QualityOfService::UserInteractive, + ) + .await + } + + #[tracing::instrument(level = "trace", skip(self))] + pub async fn did_receive_changes( + &self, + changeset: FilterChangeset, + ) -> Option { + let mut notification: Option = None; + + if let Some(filter_type) = &changeset.insert_filter { + if let Some(filter) = self.filter_from_filter_id(&filter_type.filter_id).await { + notification = Some(FilterChangesetNotificationPB::from_insert( + &self.view_id, + vec![filter], + )); + } + if let Some(filter) = self + .delegate + .get_filter(&self.view_id, &filter_type.filter_id) + .await + { + self.refresh_filters(vec![filter]).await; + } + } + + if let Some(updated_filter_type) = changeset.update_filter { + if let Some(old_filter_type) = updated_filter_type.old { + let new_filter = self + .filter_from_filter_id(&updated_filter_type.new.filter_id) + .await; + let old_filter = self.filter_from_filter_id(&old_filter_type.filter_id).await; + + // Get the filter id + let mut filter_id = old_filter.map(|filter| filter.id); + if filter_id.is_none() { + filter_id = new_filter.as_ref().map(|filter| filter.id.clone()); + } + + if let Some(filter_id) = filter_id { + // Update the corresponding filter in the cache + if let Some(filter) = self.delegate.get_filter(&self.view_id, &filter_id).await { + self.refresh_filters(vec![filter]).await; + } + + notification = Some(FilterChangesetNotificationPB::from_update( + &self.view_id, + vec![UpdatedFilter { + filter_id, + filter: new_filter, + }], + )); + } + } + } + + if let Some(filter_type) = &changeset.delete_filter { + if let Some(filter) = self.filter_from_filter_id(&filter_type.filter_id).await { + notification = Some(FilterChangesetNotificationPB::from_delete( + &self.view_id, + vec![filter], + )); + } + self.cell_filter_cache.write().remove(&filter_type.field_id); + } + + self + .gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background) + .await; + tracing::trace!("{:?}", notification); + notification + } + + async fn filter_from_filter_id(&self, filter_id: &str) -> Option { + self + .delegate + .get_filter(&self.view_id, filter_id) + .await + .map(|filter| FilterPB::from(filter.as_ref())) + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn refresh_filters(&self, filters: Vec>) { + for filter in filters { + let field_id = &filter.field_id; + tracing::trace!("Create filter with type: {:?}", filter.field_type); + match &filter.field_type { + FieldType::RichText => { + self + .cell_filter_cache + .write() + .insert(field_id, TextFilterPB::from_filter(filter.as_ref())); + }, + FieldType::Number => { + self + .cell_filter_cache + .write() + .insert(field_id, NumberFilterPB::from_filter(filter.as_ref())); + }, + FieldType::DateTime => { + self + .cell_filter_cache + .write() + .insert(field_id, DateFilterPB::from_filter(filter.as_ref())); + }, + FieldType::SingleSelect | FieldType::MultiSelect => { + self + .cell_filter_cache + .write() + .insert(field_id, SelectOptionFilterPB::from_filter(filter.as_ref())); + }, + FieldType::Checkbox => { + self + .cell_filter_cache + .write() + .insert(field_id, CheckboxFilterPB::from_filter(filter.as_ref())); + }, + FieldType::URL => { + self + .cell_filter_cache + .write() + .insert(field_id, TextFilterPB::from_filter(filter.as_ref())); + }, + FieldType::Checklist => { + self + .cell_filter_cache + .write() + .insert(field_id, ChecklistFilterPB::from_filter(filter.as_ref())); + }, + } + } + } +} + +/// Returns None if there is no change in this row after applying the filter +#[tracing::instrument(level = "trace", skip_all)] +fn filter_row( + row: &Row, + result_by_row_id: &DashMap, + field_by_field_id: &HashMap>, + cell_data_cache: &CellCache, + cell_filter_cache: &CellFilterCache, +) -> Option<(RowId, bool)> { + // Create a filter result cache if it's not exist + let mut filter_result = result_by_row_id + .entry(row.id) + .or_insert_with(FilterResult::default); + let old_is_visible = filter_result.is_visible(); + + // Iterate each cell of the row to check its visibility + for (field_id, field) in field_by_field_id { + if !cell_filter_cache.read().contains(field_id) { + filter_result.visible_by_field_id.remove(field_id); + continue; + } + + let cell = row.cells.get(field_id).cloned(); + let field_type = FieldType::from(field.field_type); + // if the visibility of the cell_rew is changed, which means the visibility of the + // row is changed too. + if let Some(is_visible) = + filter_cell(&field_type, field, cell, cell_data_cache, cell_filter_cache) + { + filter_result + .visible_by_field_id + .insert(field_id.to_string(), is_visible); + } + } + + let is_visible = filter_result.is_visible(); + if old_is_visible != is_visible { + Some((row.id, is_visible)) + } else { + None + } +} + +// Returns None if there is no change in this cell after applying the filter +// Returns Some if the visibility of the cell is changed + +#[tracing::instrument(level = "trace", skip_all, fields(cell_content))] +fn filter_cell( + field_type: &FieldType, + field: &Arc, + cell: Option, + cell_data_cache: &CellCache, + cell_filter_cache: &CellFilterCache, +) -> Option { + let handler = TypeOptionCellExt::new( + field.as_ref(), + Some(cell_data_cache.clone()), + Some(cell_filter_cache.clone()), + ) + .get_type_option_cell_data_handler(field_type)?; + let is_visible = + handler.handle_cell_filter(field_type, field.as_ref(), &cell.unwrap_or_default()); + Some(is_visible) +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +enum FilterEvent { + FilterDidChanged, + RowDidChanged(RowId), +} + +impl ToString for FilterEvent { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl FromStr for FilterEvent { + type Err = serde_json::Error; + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs new file mode 100644 index 0000000000..befb378d0e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -0,0 +1,175 @@ +use anyhow::bail; +use collab::core::any_map::AnyMapExtension; +use collab_database::views::{FilterMap, FilterMapBuilder}; + +use crate::entities::{DeleteFilterParams, FieldType, FilterPB, InsertedRowPB}; + +#[derive(Debug, Clone)] +pub struct Filter { + pub id: String, + pub field_id: String, + pub field_type: FieldType, + pub condition: i64, + pub content: String, +} + +const FILTER_ID: &str = "id"; +const FIELD_ID: &str = "field_id"; +const FIELD_TYPE: &str = "ty"; +const FILTER_CONDITION: &str = "condition"; +const FILTER_CONTENT: &str = "content"; + +impl From for FilterMap { + fn from(data: Filter) -> Self { + FilterMapBuilder::new() + .insert_str_value(FILTER_ID, data.id) + .insert_str_value(FIELD_ID, data.field_id) + .insert_str_value(FILTER_CONTENT, data.content) + .insert_i64_value(FIELD_TYPE, data.field_type.into()) + .insert_i64_value(FILTER_CONDITION, data.condition) + .build() + } +} + +impl TryFrom for Filter { + type Error = anyhow::Error; + + fn try_from(filter: FilterMap) -> Result { + match ( + filter.get_str_value(FILTER_ID), + filter.get_str_value(FIELD_ID), + ) { + (Some(id), Some(field_id)) => { + let condition = filter.get_i64_value(FILTER_CONDITION).unwrap_or(0); + let content = filter.get_str_value(FILTER_CONTENT).unwrap_or_default(); + let field_type = filter + .get_i64_value(FIELD_TYPE) + .map(FieldType::from) + .unwrap_or_default(); + Ok(Filter { + id, + field_id, + field_type, + condition, + content, + }) + }, + _ => { + bail!("Invalid filter data") + }, + } + } +} +#[derive(Debug)] +pub struct FilterChangeset { + pub(crate) insert_filter: Option, + pub(crate) update_filter: Option, + pub(crate) delete_filter: Option, +} + +#[derive(Debug)] +pub struct UpdatedFilterType { + pub old: Option, + pub new: FilterType, +} + +impl UpdatedFilterType { + pub fn new(old: Option, new: FilterType) -> UpdatedFilterType { + Self { old, new } + } +} + +impl FilterChangeset { + pub fn from_insert(filter_type: FilterType) -> Self { + Self { + insert_filter: Some(filter_type), + update_filter: None, + delete_filter: None, + } + } + + pub fn from_update(filter_type: UpdatedFilterType) -> Self { + Self { + insert_filter: None, + update_filter: Some(filter_type), + delete_filter: None, + } + } + pub fn from_delete(filter_type: FilterType) -> Self { + Self { + insert_filter: None, + update_filter: None, + delete_filter: Some(filter_type), + } + } +} + +#[derive(Hash, Eq, PartialEq, Debug, Clone)] +pub struct FilterType { + pub filter_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl std::convert::From<&Filter> for FilterType { + fn from(filter: &Filter) -> Self { + Self { + filter_id: filter.id.clone(), + field_id: filter.field_id.clone(), + field_type: filter.field_type.clone(), + } + } +} + +impl std::convert::From<&FilterPB> for FilterType { + fn from(filter: &FilterPB) -> Self { + Self { + filter_id: filter.id.clone(), + field_id: filter.field_id.clone(), + field_type: filter.field_type.clone(), + } + } +} +// #[derive(Hash, Eq, PartialEq, Debug, Clone)] +// pub struct InsertedFilterType { +// pub field_id: String, +// pub filter_id: Option, +// pub field_type: FieldType, +// } +// +// impl std::convert::From<&Filter> for InsertedFilterType { +// fn from(params: &Filter) -> Self { +// Self { +// field_id: params.field_id.clone(), +// filter_id: Some(params.id.clone()), +// field_type: params.field_type.clone(), +// } +// } +// } + +impl std::convert::From<&DeleteFilterParams> for FilterType { + fn from(params: &DeleteFilterParams) -> Self { + params.filter_type.clone() + } +} + +#[derive(Clone, Debug)] +pub struct FilterResultNotification { + pub view_id: String, + + // Indicates there will be some new rows being visible from invisible state. + pub visible_rows: Vec, + + // Indicates there will be some new rows being invisible from visible state. + pub invisible_rows: Vec, +} + +impl FilterResultNotification { + pub fn new(view_id: String) -> Self { + Self { + view_id, + visible_rows: vec![], + invisible_rows: vec![], + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/mod.rs b/frontend/rust-lib/flowy-database2/src/services/filter/mod.rs new file mode 100644 index 0000000000..72bfa3a925 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/filter/mod.rs @@ -0,0 +1,7 @@ +mod controller; +mod entities; +mod task; + +pub use controller::*; +pub use entities::*; +pub(crate) use task::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/task.rs b/frontend/rust-lib/flowy-database2/src/services/filter/task.rs new file mode 100644 index 0000000000..f53a0554a2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/filter/task.rs @@ -0,0 +1,60 @@ +use crate::services::filter::FilterController; +use flowy_task::{TaskContent, TaskHandler}; +use lib_infra::future::BoxResultFuture; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct FilterTaskHandler { + handler_id: String, + filter_controller: Arc, +} + +impl FilterTaskHandler { + pub fn new(handler_id: String, filter_controller: Arc) -> Self { + Self { + handler_id, + filter_controller, + } + } +} + +impl TaskHandler for FilterTaskHandler { + fn handler_id(&self) -> &str { + &self.handler_id + } + + fn handler_name(&self) -> &str { + "FilterTaskHandler" + } + + fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> { + let filter_controller = self.filter_controller.clone(); + Box::pin(async move { + if let TaskContent::Text(predicate) = content { + filter_controller + .process(&predicate) + .await + .map_err(anyhow::Error::from)?; + } + Ok(()) + }) + } +} +/// Refresh the filter according to the field id. +#[derive(Default)] +pub(crate) struct FilterResult { + pub(crate) visible_by_field_id: HashMap, +} + +impl FilterResult { + pub(crate) fn is_visible(&self) -> bool { + let mut is_visible = true; + for visible in self.visible_by_field_id.values() { + if !is_visible { + break; + } + is_visible = *visible; + } + is_visible + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs new file mode 100644 index 0000000000..f7800ff606 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -0,0 +1,117 @@ +use crate::entities::{GroupChangesetPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; +use crate::services::cell::DecodedCellData; +use crate::services::group::controller::MoveGroupRowContext; +use crate::services::group::GroupData; +use collab_database::fields::Field; +use collab_database::rows::{Cell, Row}; + +use flowy_error::FlowyResult; + +/// Using polymorphism to provides the customs action for different group controller. +/// +/// For example, the `CheckboxGroupController` implements this trait to provide custom behavior. +/// +pub trait GroupCustomize: Send + Sync { + type CellData: DecodedCellData; + /// Returns the a value of the cell if the cell data is not exist. + /// The default value is `None` + /// + /// Determine which group the row is placed in based on the data of the cell. If the cell data + /// is None. The row will be put in to the `No status` group + /// + fn placeholder_cell(&self) -> Option { + None + } + + /// Returns a bool value to determine whether the group should contain this cell or not. + fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool; + + fn create_or_delete_group_when_cell_changed( + &mut self, + _row: &Row, + _old_cell_data: Option<&Self::CellData>, + _cell_data: &Self::CellData, + ) -> FlowyResult<(Option, Option)> { + Ok((None, None)) + } + + /// Adds or removes a row if the cell data match the group filter. + /// It gets called after editing the cell or row + /// + fn add_or_remove_row_when_cell_changed( + &mut self, + row: &Row, + cell_data: &Self::CellData, + ) -> Vec; + + /// Deletes the row from the group + fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec; + + /// Move row from one group to another + fn move_row( + &mut self, + cell_data: &Self::CellData, + context: MoveGroupRowContext, + ) -> Vec; + + /// Returns None if there is no need to delete the group when corresponding row get removed + fn delete_group_when_move_row( + &mut self, + _row: &Row, + _cell_data: &Self::CellData, + ) -> Option { + None + } +} + +/// Defines the shared actions any group controller can perform. +pub trait GroupControllerActions: Send + Sync { + /// The field that is used for grouping the rows + fn field_id(&self) -> &str; + + /// Returns number of groups the current field has + fn groups(&self) -> Vec<&GroupData>; + + /// Returns the index and the group data with group_id + fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)>; + + /// Separates the rows into different groups + fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()>; + + /// Remove the group with from_group_id and insert it to the index with to_group_id + fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>; + + /// Insert/Remove the row to the group if the corresponding cell data is changed + fn did_update_group_row( + &mut self, + old_row: &Option, + row: &Row, + field: &Field, + ) -> FlowyResult; + + /// Remove the row from the group if the row gets deleted + fn did_delete_delete_row( + &mut self, + row: &Row, + field: &Field, + ) -> FlowyResult; + + /// Move the row from one group to another group + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult; + + /// Update the group if the corresponding field is changed + fn did_update_group_field(&mut self, field: &Field) -> FlowyResult>; +} + +#[derive(Debug)] +pub struct DidUpdateGroupRowResult { + pub(crate) inserted_group: Option, + pub(crate) deleted_group: Option, + pub(crate) row_changesets: Vec, +} + +#[derive(Debug)] +pub struct DidMoveGroupRowResult { + pub(crate) deleted_group: Option, + pub(crate) row_changesets: Vec, +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs new file mode 100644 index 0000000000..b62a8a5b63 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -0,0 +1,481 @@ +use crate::entities::{GroupChangesetPB, GroupPB, InsertedGroupPB}; +use crate::services::field::RowSingleCellData; +use crate::services::group::{ + default_group_setting, GeneratedGroupContext, Group, GroupData, GroupSetting, +}; +use collab_database::fields::Field; +use flowy_error::{FlowyError, FlowyResult}; +use indexmap::IndexMap; +use lib_infra::future::Fut; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::fmt::Formatter; +use std::marker::PhantomData; +use std::sync::Arc; + +pub trait GroupSettingReader: Send + Sync + 'static { + fn get_group_setting(&self, view_id: &str) -> Fut>>; + fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut>; +} + +pub trait GroupSettingWriter: Send + Sync + 'static { + fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut>; +} + +impl std::fmt::Display for GroupContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.groups_map.iter().for_each(|(_, group)| { + let _ = f.write_fmt(format_args!( + "Group:{} has {} rows \n", + group.id, + group.rows.len() + )); + }); + + Ok(()) + } +} + +/// A [GroupContext] represents as the groups memory cache +/// Each [GenericGroupController] has its own [GroupContext], the `context` has its own configuration +/// that is restored from the disk. +/// +/// The `context` contains a list of [GroupData]s and the grouping [Field] +pub struct GroupContext { + pub view_id: String, + /// The group configuration restored from the disk. + /// + /// Uses the [GroupSettingReader] to read the configuration data from disk + setting: Arc, + + configuration_phantom: PhantomData, + + /// The grouping field + field: Arc, + + /// Cache all the groups + groups_map: IndexMap, + + /// A reader that implement the [GroupSettingReader] trait + /// + #[allow(dead_code)] + reader: Arc, + + /// A writer that implement the [GroupSettingWriter] trait is used to save the + /// configuration to disk + /// + writer: Arc, +} + +impl GroupContext +where + C: Serialize + DeserializeOwned, +{ + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn new( + view_id: String, + field: Arc, + reader: Arc, + writer: Arc, + ) -> FlowyResult { + let setting = match reader.get_group_setting(&view_id).await { + None => { + let default_configuration = default_group_setting(&field); + writer + .save_configuration(&view_id, default_configuration.clone()) + .await?; + Arc::new(default_configuration) + }, + Some(setting) => setting, + }; + + Ok(Self { + view_id, + field, + groups_map: IndexMap::new(), + reader, + writer, + setting, + configuration_phantom: PhantomData, + }) + } + + /// Returns the no `status` group + /// + /// We take the `id` of the `field` as the no status group id + pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> { + self.groups_map.get(&self.field.id) + } + + pub(crate) fn get_mut_no_status_group(&mut self) -> Option<&mut GroupData> { + self.groups_map.get_mut(&self.field.id) + } + + pub(crate) fn groups(&self) -> Vec<&GroupData> { + self.groups_map.values().collect() + } + + pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut GroupData> { + self.groups_map.get_mut(group_id) + } + + // Returns the index and group specified by the group_id + pub(crate) fn get_group(&self, group_id: &str) -> Option<(usize, &GroupData)> { + match ( + self.groups_map.get_index_of(group_id), + self.groups_map.get(group_id), + ) { + (Some(index), Some(group)) => Some((index, group)), + _ => None, + } + } + + /// Iterate mut the groups without `No status` group + pub(crate) fn iter_mut_status_groups(&mut self, mut each: impl FnMut(&mut GroupData)) { + self.groups_map.iter_mut().for_each(|(_, group)| { + if group.id != self.field.id { + each(group); + } + }); + } + + pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut GroupData)) { + self.groups_map.iter_mut().for_each(|(_, group)| { + each(group); + }); + } + #[tracing::instrument(level = "trace", skip(self), err)] + pub(crate) fn add_new_group(&mut self, group: Group) -> FlowyResult { + let group_data = GroupData::new( + group.id.clone(), + self.field.id.clone(), + group.name.clone(), + group.id.clone(), + ); + self.groups_map.insert(group.id.clone(), group_data); + let (index, group_data) = self.get_group(&group.id).unwrap(); + let insert_group = InsertedGroupPB { + group: GroupPB::from(group_data.clone()), + index: index as i32, + }; + + self.mut_configuration(|configuration| { + configuration.groups.push(group); + true + })?; + + Ok(insert_group) + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(crate) fn delete_group(&mut self, deleted_group_id: &str) -> FlowyResult<()> { + self.groups_map.remove(deleted_group_id); + self.mut_configuration(|configuration| { + configuration + .groups + .retain(|group| group.id != deleted_group_id); + true + })?; + Ok(()) + } + + pub(crate) fn move_group(&mut self, from_id: &str, to_id: &str) -> FlowyResult<()> { + let from_index = self.groups_map.get_index_of(from_id); + let to_index = self.groups_map.get_index_of(to_id); + match (from_index, to_index) { + (Some(from_index), Some(to_index)) => { + self.groups_map.move_index(from_index, to_index); + + self.mut_configuration(|configuration| { + let from_index = configuration + .groups + .iter() + .position(|group| group.id == from_id); + let to_index = configuration + .groups + .iter() + .position(|group| group.id == to_id); + if let (Some(from), Some(to)) = &(from_index, to_index) { + tracing::trace!( + "Move group from index:{:?} to index:{:?}", + from_index, + to_index + ); + let group = configuration.groups.remove(*from); + configuration.groups.insert(*to, group); + } + tracing::debug!( + "Group order: {:?} ", + configuration + .groups + .iter() + .map(|group| group.name.clone()) + .collect::>() + .join(",") + ); + + from_index.is_some() && to_index.is_some() + })?; + Ok(()) + }, + _ => Err(FlowyError::record_not_found().context("Moving group failed. Groups are not exist")), + } + } + + /// Reset the memory cache of the groups and update the group configuration + /// + /// # Arguments + /// + /// * `generated_group_configs`: the generated groups contains a list of [GeneratedGroupConfig]. + /// + /// Each [FieldType] can implement the [GroupGenerator] trait in order to generate different + /// groups. For example, the FieldType::Checkbox has the [CheckboxGroupGenerator] that implements + /// the [GroupGenerator] trait. + /// + /// Consider the passed-in generated_group_configs as new groups, the groups in the current + /// [GroupConfigurationRevision] as old groups. The old groups and the new groups will be merged + /// while keeping the order of the old groups. + /// + #[tracing::instrument(level = "trace", skip(self, generated_group_context), err)] + pub(crate) fn init_groups( + &mut self, + generated_group_context: GeneratedGroupContext, + ) -> FlowyResult> { + let GeneratedGroupContext { + no_status_group, + group_configs, + } = generated_group_context; + + let mut new_groups = vec![]; + let mut filter_content_map = HashMap::new(); + group_configs.into_iter().for_each(|generate_group| { + filter_content_map.insert( + generate_group.group.id.clone(), + generate_group.filter_content, + ); + new_groups.push(generate_group.group); + }); + + let mut old_groups = self.setting.groups.clone(); + // clear all the groups if grouping by a new field + if self.setting.field_id != self.field.id { + old_groups.clear(); + } + + // The `all_group_revs` is the combination of the new groups and old groups + let MergeGroupResult { + mut all_groups, + new_groups, + deleted_groups, + } = merge_groups(no_status_group, old_groups, new_groups); + + let deleted_group_ids = deleted_groups + .into_iter() + .map(|group_rev| group_rev.id) + .collect::>(); + + self.mut_configuration(|configuration| { + let mut is_changed = !deleted_group_ids.is_empty(); + // Remove the groups + configuration + .groups + .retain(|group| !deleted_group_ids.contains(&group.id)); + + // Update/Insert new groups + for group in &mut all_groups { + match configuration + .groups + .iter() + .position(|old_group_rev| old_group_rev.id == group.id) + { + None => { + // Push the group to the end of the list if it doesn't exist in the group + configuration.groups.push(group.clone()); + is_changed = true; + }, + Some(pos) => { + let mut old_group = configuration.groups.get_mut(pos).unwrap(); + // Take the old group setting + group.visible = old_group.visible; + if !is_changed { + is_changed = is_group_changed(group, old_group); + } + // Consider the the name of the `group_rev` as the newest. + old_group.name = group.name.clone(); + }, + } + } + is_changed + })?; + + // Update the memory cache of the groups + all_groups.into_iter().for_each(|group_rev| { + let filter_content = filter_content_map + .get(&group_rev.id) + .cloned() + .unwrap_or_else(|| "".to_owned()); + let group = GroupData::new( + group_rev.id, + self.field.id.clone(), + group_rev.name, + filter_content, + ); + self.groups_map.insert(group.id.clone(), group); + }); + + let initial_groups = new_groups + .into_iter() + .flat_map(|group_rev| { + let filter_content = filter_content_map.get(&group_rev.id)?; + let group = GroupData::new( + group_rev.id, + self.field.id.clone(), + group_rev.name, + filter_content.clone(), + ); + Some(GroupPB::from(group)) + }) + .collect(); + + let changeset = GroupChangesetPB { + view_id: self.view_id.clone(), + initial_groups, + deleted_groups: deleted_group_ids, + update_groups: vec![], + inserted_groups: vec![], + }; + tracing::trace!("Group changeset: {:?}", changeset); + if changeset.is_empty() { + Ok(None) + } else { + Ok(Some(changeset)) + } + } + + #[allow(dead_code)] + pub(crate) async fn hide_group(&mut self, group_id: &str) -> FlowyResult<()> { + self.mut_group_rev(group_id, |group_rev| { + group_rev.visible = false; + })?; + Ok(()) + } + + #[allow(dead_code)] + pub(crate) async fn show_group(&mut self, group_id: &str) -> FlowyResult<()> { + self.mut_group_rev(group_id, |group_rev| { + group_rev.visible = true; + })?; + Ok(()) + } + + pub(crate) async fn get_all_cells(&self) -> Vec { + self + .reader + .get_configuration_cells(&self.view_id, &self.field.id) + .await + } + + fn mut_configuration( + &mut self, + mut_configuration_fn: impl FnOnce(&mut GroupSetting) -> bool, + ) -> FlowyResult<()> { + let configuration = Arc::make_mut(&mut self.setting); + let is_changed = mut_configuration_fn(configuration); + if is_changed { + let configuration = (*self.setting).clone(); + let writer = self.writer.clone(); + let field_id = self.field.id.clone(); + tokio::spawn(async move { + match writer.save_configuration(&field_id, configuration).await { + Ok(_) => {}, + Err(e) => { + tracing::error!("Save group configuration failed: {}", e); + }, + } + }); + } + Ok(()) + } + + fn mut_group_rev( + &mut self, + group_id: &str, + mut_groups_fn: impl Fn(&mut Group), + ) -> FlowyResult<()> { + self.mut_configuration(|configuration| { + match configuration + .groups + .iter_mut() + .find(|group| group.id == group_id) + { + None => false, + Some(group) => { + mut_groups_fn(group); + true + }, + } + }) + } +} + +/// Merge the new groups into old groups while keeping the order in the old groups +/// +fn merge_groups( + no_status_group: Option, + old_groups: Vec, + new_groups: Vec, +) -> MergeGroupResult { + let mut merge_result = MergeGroupResult::new(); + // group_map is a helper map is used to filter out the new groups. + let mut new_group_map: IndexMap = IndexMap::new(); + new_groups.into_iter().for_each(|group_rev| { + new_group_map.insert(group_rev.id.clone(), group_rev); + }); + + // The group is ordered in old groups. Add them before adding the new groups + for old in old_groups { + if let Some(new) = new_group_map.remove(&old.id) { + merge_result.all_groups.push(new.clone()); + } else { + merge_result.deleted_groups.push(old); + } + } + + // Find out the new groups + let new_groups = new_group_map.into_values(); + for (_, group) in new_groups.into_iter().enumerate() { + merge_result.all_groups.push(group.clone()); + merge_result.new_groups.push(group); + } + + // The `No status` group index is initialized to 0 + if let Some(no_status_group) = no_status_group { + merge_result.all_groups.insert(0, no_status_group); + } + merge_result +} + +fn is_group_changed(new: &Group, old: &Group) -> bool { + if new.name != old.name { + return true; + } + false +} + +struct MergeGroupResult { + // Contains the new groups and the updated groups + all_groups: Vec, + new_groups: Vec, + deleted_groups: Vec, +} + +impl MergeGroupResult { + fn new() -> Self { + Self { + all_groups: vec![], + new_groups: vec![], + deleted_groups: vec![], + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs new file mode 100644 index 0000000000..edf1e5863d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -0,0 +1,382 @@ +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; + +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{Cell, Cells, Row, RowId}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use flowy_error::FlowyResult; + +use crate::entities::{FieldType, GroupChangesetPB, GroupRowsNotificationPB, InsertedRowPB}; +use crate::services::cell::{get_type_cell_protobuf, CellProtobufBlobParser, DecodedCellData}; +use crate::services::group::action::{ + DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerActions, GroupCustomize, +}; +use crate::services::group::configuration::GroupContext; +use crate::services::group::entities::GroupData; +use crate::services::group::Group; + +// use collab_database::views::Group; + +/// The [GroupController] trait defines the group actions, including create/delete/move items +/// For example, the group will insert a item if the one of the new [RowRevision]'s [CellRevision]s +/// content match the group filter. +/// +/// Different [FieldType] has a different controller that implements the [GroupController] trait. +/// If the [FieldType] doesn't implement its group controller, then the [DefaultGroupController] will +/// be used. +/// +pub trait GroupController: GroupControllerActions + Send + Sync { + fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str); + fn did_create_row(&mut self, row: &Row, group_id: &str); +} + +/// The [GroupGenerator] trait is used to generate the groups for different [FieldType] +pub trait GroupGenerator { + type Context; + type TypeOptionType; + + fn generate_groups( + field: &Field, + group_ctx: &Self::Context, + type_option: &Option, + ) -> GeneratedGroupContext; +} + +pub struct GeneratedGroupContext { + pub no_status_group: Option, + pub group_configs: Vec, +} + +pub struct GeneratedGroupConfig { + pub group: Group, + pub filter_content: String, +} + +pub struct MoveGroupRowContext<'a> { + pub row: &'a Row, + pub row_changeset: &'a mut RowChangeset, + pub field: &'a Field, + pub to_group_id: &'a str, + pub to_row_id: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct RowChangeset { + pub row_id: RowId, + pub height: Option, + pub visibility: Option, + // Contains the key/value changes represents as the update of the cells. For example, + // if there is one cell was changed, then the `cell_by_field_id` will only have one key/value. + pub cell_by_field_id: HashMap, +} + +impl RowChangeset { + pub fn new(row_id: RowId) -> Self { + Self { + row_id, + ..Default::default() + } + } + + pub fn is_empty(&self) -> bool { + self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty() + } +} + +/// C: represents the group configuration that impl [GroupConfigurationSerde] +/// T: the type-option data deserializer that impl [TypeOptionDataDeserializer] +/// G: the group generator, [GroupGenerator] +/// P: the parser that impl [CellProtobufBlobParser] for the CellBytes +pub struct GenericGroupController { + pub grouping_field_id: String, + pub type_option: Option, + pub group_ctx: GroupContext, + group_action_phantom: PhantomData, + cell_parser_phantom: PhantomData

, +} + +impl GenericGroupController +where + C: Serialize + DeserializeOwned, + T: From, + G: GroupGenerator, TypeOptionType = T>, +{ + pub async fn new( + grouping_field: &Arc, + mut configuration: GroupContext, + ) -> FlowyResult { + let field_type = FieldType::from(grouping_field.field_type); + let type_option = grouping_field.get_type_option::(field_type); + let generated_group_context = G::generate_groups(grouping_field, &configuration, &type_option); + let _ = configuration.init_groups(generated_group_context)?; + + Ok(Self { + grouping_field_id: grouping_field.id.clone(), + type_option, + group_ctx: configuration, + group_action_phantom: PhantomData, + cell_parser_phantom: PhantomData, + }) + } + + // https://stackoverflow.com/questions/69413164/how-to-fix-this-clippy-warning-needless-collect + #[allow(clippy::needless_collect)] + fn update_no_status_group( + &mut self, + row: &Row, + other_group_changesets: &[GroupRowsNotificationPB], + ) -> Option { + let no_status_group = self.group_ctx.get_mut_no_status_group()?; + + // [other_group_inserted_row] contains all the inserted rows except the default group. + let other_group_inserted_row = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.inserted_rows) + .collect::>(); + + // Calculate the inserted_rows of the default_group + let no_status_group_rows = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.deleted_rows) + .cloned() + .filter(|row_id| { + // if the [other_group_inserted_row] contains the row_id of the row + // which means the row should not move to the default group. + !other_group_inserted_row + .iter() + .any(|inserted_row| &inserted_row.row.id == row_id) + }) + .collect::>(); + + let mut changeset = GroupRowsNotificationPB::new(no_status_group.id.clone()); + if !no_status_group_rows.is_empty() { + changeset.inserted_rows.push(InsertedRowPB::new(row.into())); + no_status_group.add_row(row.clone()); + } + + // [other_group_delete_rows] contains all the deleted rows except the default group. + let other_group_delete_rows: Vec = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.deleted_rows) + .cloned() + .collect(); + + let default_group_deleted_rows = other_group_changesets + .iter() + .flat_map(|changeset| &changeset.inserted_rows) + .filter(|inserted_row| { + // if the [other_group_delete_rows] contain the inserted_row, which means this row should move + // out from the default_group. + !other_group_delete_rows + .iter() + .any(|row_id| inserted_row.row.id == *row_id) + }) + .collect::>(); + + let mut deleted_row_ids = vec![]; + for row in &no_status_group.rows { + let row_id: i64 = row.id.into(); + if default_group_deleted_rows + .iter() + .any(|deleted_row| deleted_row.row.id == row_id) + { + deleted_row_ids.push(row.id); + } + } + no_status_group + .rows + .retain(|row| !deleted_row_ids.contains(&row.id)); + changeset.deleted_rows.extend( + deleted_row_ids + .into_iter() + .map(|id| id.into()) + .collect::>(), + ); + Some(changeset) + } +} + +impl GroupControllerActions for GenericGroupController +where + P: CellProtobufBlobParser, + C: Serialize + DeserializeOwned, + T: From, + G: GroupGenerator, TypeOptionType = T>, + + Self: GroupCustomize, +{ + fn field_id(&self) -> &str { + &self.grouping_field_id + } + + fn groups(&self) -> Vec<&GroupData> { + self.group_ctx.groups() + } + + fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)> { + let group = self.group_ctx.get_group(group_id)?; + Some((group.0, group.1.clone())) + } + + #[tracing::instrument(level = "trace", skip_all, fields(row_count=%rows.len(), group_result))] + fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()> { + for row in rows { + let cell = match row.cells.get(&self.grouping_field_id) { + None => self.placeholder_cell(), + Some(cell) => Some(cell.clone()), + }; + + if let Some(cell) = cell { + let mut grouped_rows: Vec = vec![]; + let cell_bytes = get_type_cell_protobuf(&cell, field, None); + let cell_data = cell_bytes.parser::

()?; + for group in self.group_ctx.groups() { + if self.can_group(&group.filter_content, &cell_data) { + grouped_rows.push(GroupedRow { + row: (*row).clone(), + group_id: group.id.clone(), + }); + } + } + + if !grouped_rows.is_empty() { + for group_row in grouped_rows { + if let Some(group) = self.group_ctx.get_mut_group(&group_row.group_id) { + group.add_row(group_row.row); + } + } + continue; + } + } + match self.group_ctx.get_mut_no_status_group() { + None => {}, + Some(no_status_group) => no_status_group.add_row((*row).clone()), + } + } + + tracing::Span::current().record("group_result", format!("{},", self.group_ctx,).as_str()); + Ok(()) + } + + fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { + self.group_ctx.move_group(from_group_id, to_group_id) + } + + fn did_update_group_row( + &mut self, + old_row: &Option, + row: &Row, + field: &Field, + ) -> FlowyResult { + // let cell_data = row_rev.cells.get(&self.field_id).and_then(|cell_rev| { + // let cell_data: Option

= get_type_cell_data(cell_rev, field_rev, None); + // cell_data + // }); + let mut result = DidUpdateGroupRowResult { + inserted_group: None, + deleted_group: None, + row_changesets: vec![], + }; + + if let Some(cell_data) = get_cell_data_from_row::

(Some(row), field) { + let old_row = old_row.as_ref(); + let old_cell_data = get_cell_data_from_row::

(old_row, field); + if let Ok((insert, delete)) = + self.create_or_delete_group_when_cell_changed(row, old_cell_data.as_ref(), &cell_data) + { + result.inserted_group = insert; + result.deleted_group = delete; + } + + let mut changesets = self.add_or_remove_row_when_cell_changed(row, &cell_data); + if let Some(changeset) = self.update_no_status_group(row, &changesets) { + if !changeset.is_empty() { + changesets.push(changeset); + } + } + result.row_changesets = changesets; + } + + Ok(result) + } + + fn did_delete_delete_row( + &mut self, + row: &Row, + field: &Field, + ) -> FlowyResult { + // if the cell_rev is none, then the row must in the default group. + let mut result = DidMoveGroupRowResult { + deleted_group: None, + row_changesets: vec![], + }; + if let Some(cell) = row.cells.get(&self.grouping_field_id) { + let cell_bytes = get_type_cell_protobuf(cell, field, None); + let cell_data = cell_bytes.parser::

()?; + if !cell_data.is_empty() { + tracing::error!("did_delete_delete_row {:?}", cell); + result.row_changesets = self.delete_row(row, &cell_data); + return Ok(result); + } + } + + match self.group_ctx.get_no_status_group() { + None => { + tracing::error!("Unexpected None value. It should have the no status group"); + }, + Some(no_status_group) => { + if !no_status_group.contains_row(row.id) { + tracing::error!("The row: {:?} should be in the no status group", row.id); + } + result.row_changesets = vec![GroupRowsNotificationPB::delete( + no_status_group.id.clone(), + vec![row.id.into()], + )]; + }, + } + Ok(result) + } + + #[tracing::instrument(level = "trace", skip_all, err)] + fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult { + let mut result = DidMoveGroupRowResult { + deleted_group: None, + row_changesets: vec![], + }; + let cell_rev = match context.row.cells.get(&self.grouping_field_id) { + Some(cell_rev) => Some(cell_rev.clone()), + None => self.placeholder_cell(), + }; + + if let Some(cell) = cell_rev { + let cell_bytes = get_type_cell_protobuf(&cell, context.field, None); + let cell_data = cell_bytes.parser::

()?; + result.deleted_group = self.delete_group_when_move_row(context.row, &cell_data); + result.row_changesets = self.move_row(&cell_data, context); + } else { + tracing::warn!("Unexpected moving group row, changes should not be empty"); + } + Ok(result) + } + + fn did_update_group_field(&mut self, _field: &Field) -> FlowyResult> { + Ok(None) + } +} + +struct GroupedRow { + row: Row, + group_id: String, +} + +fn get_cell_data_from_row( + row: Option<&Row>, + field: &Field, +) -> Option { + let cell = row.and_then(|row| row.cells.get(&field.id))?; + let cell_bytes = get_type_cell_protobuf(cell, field, None); + cell_bytes.parser::

().ok() +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs new file mode 100644 index 0000000000..eb834801c9 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -0,0 +1,172 @@ +use collab_database::fields::Field; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use serde::{Deserialize, Serialize}; + +use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowPB}; +use crate::services::cell::insert_checkbox_cell; +use crate::services::field::{ + CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOption, CHECK, UNCHECK, +}; +use crate::services::group::action::GroupCustomize; +use crate::services::group::configuration::GroupContext; +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; +use crate::services::group::{move_group_row, GeneratedGroupConfig, GeneratedGroupContext, Group}; + +#[derive(Default, Serialize, Deserialize)] +pub struct CheckboxGroupConfiguration { + pub hide_empty: bool, +} + +pub type CheckboxGroupController = GenericGroupController< + CheckboxGroupConfiguration, + CheckboxTypeOption, + CheckboxGroupGenerator, + CheckboxCellDataParser, +>; + +pub type CheckboxGroupContext = GroupContext; + +impl GroupCustomize for CheckboxGroupController { + type CellData = CheckboxCellData; + fn placeholder_cell(&self) -> Option { + Some( + new_cell_builder(FieldType::Checkbox) + .insert_str_value("data", UNCHECK) + .build(), + ) + } + + fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + if cell_data.is_check() { + content == CHECK + } else { + content == UNCHECK + } + } + + fn add_or_remove_row_when_cell_changed( + &mut self, + row: &Row, + cell_data: &Self::CellData, + ) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_status_groups(|group| { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + let is_not_contained = !group.contains_row(row.id); + if group.id == CHECK { + if cell_data.is_uncheck() { + // Remove the row if the group.id is CHECK but the cell_data is UNCHECK + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } else { + // Add the row to the group if the group didn't contain the row + if is_not_contained { + changeset + .inserted_rows + .push(InsertedRowPB::new(RowPB::from(row))); + group.add_row(row.clone()); + } + } + } + + if group.id == UNCHECK { + if cell_data.is_check() { + // Remove the row if the group.id is UNCHECK but the cell_data is CHECK + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } else { + // Add the row to the group if the group didn't contain the row + if is_not_contained { + changeset + .inserted_rows + .push(InsertedRowPB::new(RowPB::from(row))); + group.add_row(row.clone()); + } + } + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn delete_row(&mut self, row: &Row, _cell_data: &Self::CellData) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_groups(|group| { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + if group.contains_row(row.id) { + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn move_row( + &mut self, + _cell_data: &Self::CellData, + mut context: MoveGroupRowContext, + ) -> Vec { + let mut group_changeset = vec![]; + self.group_ctx.iter_mut_groups(|group| { + if let Some(changeset) = move_group_row(group, &mut context) { + group_changeset.push(changeset); + } + }); + group_changeset + } +} + +impl GroupController for CheckboxGroupController { + fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + match self.group_ctx.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_, group)) => { + let is_check = group.id == CHECK; + let cell = insert_checkbox_cell(is_check, field); + cells.insert(field.id.clone(), cell); + }, + } + } + + fn did_create_row(&mut self, row: &Row, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row.clone()) + } + } +} + +pub struct CheckboxGroupGenerator(); +impl GroupGenerator for CheckboxGroupGenerator { + type Context = CheckboxGroupContext; + type TypeOptionType = CheckboxTypeOption; + + fn generate_groups( + _field: &Field, + _group_ctx: &Self::Context, + _type_option: &Option, + ) -> GeneratedGroupContext { + let check_group = GeneratedGroupConfig { + group: Group::new(CHECK.to_string(), "".to_string()), + filter_content: CHECK.to_string(), + }; + + let uncheck_group = GeneratedGroupConfig { + group: Group::new(UNCHECK.to_string(), "".to_string()), + filter_content: UNCHECK.to_string(), + }; + + GeneratedGroupContext { + no_status_group: None, + group_configs: vec![check_group, uncheck_group], + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs new file mode 100644 index 0000000000..7324ad0063 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::{Cells, Row}; + +use flowy_error::FlowyResult; + +use crate::entities::GroupChangesetPB; +use crate::services::group::action::{ + DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerActions, +}; +use crate::services::group::{GroupController, GroupData, MoveGroupRowContext}; + +/// A [DefaultGroupController] is used to handle the group actions for the [FieldType] that doesn't +/// implement its own group controller. The default group controller only contains one group, which +/// means all rows will be grouped in the same group. +/// +pub struct DefaultGroupController { + pub field_id: String, + pub group: GroupData, +} + +const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController"; + +impl DefaultGroupController { + pub fn new(field: &Arc) -> Self { + let group = GroupData::new( + DEFAULT_GROUP_CONTROLLER.to_owned(), + field.id.clone(), + "".to_owned(), + "".to_owned(), + ); + Self { + field_id: field.id.clone(), + group, + } + } +} + +impl GroupControllerActions for DefaultGroupController { + fn field_id(&self) -> &str { + &self.field_id + } + + fn groups(&self) -> Vec<&GroupData> { + vec![&self.group] + } + + fn get_group(&self, _group_id: &str) -> Option<(usize, GroupData)> { + Some((0, self.group.clone())) + } + + fn fill_groups(&mut self, rows: &[&Row], _field: &Field) -> FlowyResult<()> { + rows.iter().for_each(|row| { + self.group.add_row((*row).clone()); + }); + Ok(()) + } + + fn move_group(&mut self, _from_group_id: &str, _to_group_id: &str) -> FlowyResult<()> { + Ok(()) + } + + fn did_update_group_row( + &mut self, + _old_row: &Option, + _row: &Row, + _field: &Field, + ) -> FlowyResult { + Ok(DidUpdateGroupRowResult { + inserted_group: None, + deleted_group: None, + row_changesets: vec![], + }) + } + + fn did_delete_delete_row( + &mut self, + _row: &Row, + _field: &Field, + ) -> FlowyResult { + Ok(DidMoveGroupRowResult { + deleted_group: None, + row_changesets: vec![], + }) + } + + fn move_group_row( + &mut self, + _context: MoveGroupRowContext, + ) -> FlowyResult { + Ok(DidMoveGroupRowResult { + deleted_group: None, + row_changesets: vec![], + }) + } + + fn did_update_group_field(&mut self, _field: &Field) -> FlowyResult> { + Ok(None) + } +} + +impl GroupController for DefaultGroupController { + fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} + + fn did_create_row(&mut self, _row: &Row, _group_id: &str) {} +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs new file mode 100644 index 0000000000..fb890e6d29 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs @@ -0,0 +1,9 @@ +mod checkbox_controller; +mod default_controller; +mod select_option_controller; +mod url_controller; + +pub use checkbox_controller::*; +pub use default_controller::*; +pub use select_option_controller::*; +pub use url_controller::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/mod.rs new file mode 100644 index 0000000000..0d7b8fa03e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/mod.rs @@ -0,0 +1,7 @@ +mod multi_select_controller; +mod single_select_controller; +mod util; + +pub use multi_select_controller::*; +pub use single_select_controller::*; +pub use util::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs new file mode 100644 index 0000000000..595df4ad89 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -0,0 +1,118 @@ +use crate::entities::{GroupRowsNotificationPB, SelectOptionCellDataPB}; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::{MultiSelectTypeOption, SelectOptionCellDataParser}; +use crate::services::group::action::GroupCustomize; +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; +use crate::services::group::{ + add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group, + move_group_row, remove_select_option_row, GeneratedGroupContext, GroupContext, +}; +use collab_database::fields::Field; +use collab_database::rows::{Cells, Row}; + +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize)] +pub struct MultiSelectGroupConfiguration { + pub hide_empty: bool, +} + +pub type MultiSelectOptionGroupContext = GroupContext; +// MultiSelect +pub type MultiSelectGroupController = GenericGroupController< + MultiSelectGroupConfiguration, + MultiSelectTypeOption, + MultiSelectGroupGenerator, + SelectOptionCellDataParser, +>; + +impl GroupCustomize for MultiSelectGroupController { + type CellData = SelectOptionCellDataPB; + + fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + cell_data + .select_options + .iter() + .any(|option| option.id == content) + } + + fn add_or_remove_row_when_cell_changed( + &mut self, + row: &Row, + cell_data: &Self::CellData, + ) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_status_groups(|group| { + if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row) { + changesets.push(changeset); + } + }); + changesets + } + + fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_status_groups(|group| { + if let Some(changeset) = remove_select_option_row(group, cell_data, row) { + changesets.push(changeset); + } + }); + changesets + } + + fn move_row( + &mut self, + _cell_data: &Self::CellData, + mut context: MoveGroupRowContext, + ) -> Vec { + let mut group_changeset = vec![]; + self.group_ctx.iter_mut_groups(|group| { + if let Some(changeset) = move_group_row(group, &mut context) { + group_changeset.push(changeset); + } + }); + group_changeset + } +} + +impl GroupController for MultiSelectGroupController { + fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + match self.group_ctx.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_, group)) => { + let cell = insert_select_option_cell(vec![group.id.clone()], field); + cells.insert(field.id.clone(), cell); + }, + } + } + + fn did_create_row(&mut self, row: &Row, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row.clone()) + } + } +} + +pub struct MultiSelectGroupGenerator(); +impl GroupGenerator for MultiSelectGroupGenerator { + type Context = MultiSelectOptionGroupContext; + type TypeOptionType = MultiSelectTypeOption; + + fn generate_groups( + field: &Field, + _group_ctx: &Self::Context, + type_option: &Option, + ) -> GeneratedGroupContext { + let group_configs = match type_option { + None => vec![], + Some(type_option) => generate_select_option_groups(&field.id, &type_option.options), + }; + + GeneratedGroupContext { + no_status_group: Some(make_no_status_group(field)), + group_configs, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs new file mode 100644 index 0000000000..2fbac01f03 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -0,0 +1,117 @@ +use crate::entities::{GroupRowsNotificationPB, SelectOptionCellDataPB}; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::{SelectOptionCellDataParser, SingleSelectTypeOption}; +use crate::services::group::action::GroupCustomize; +use collab_database::fields::Field; +use collab_database::rows::{Cells, Row}; + +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; +use crate::services::group::controller_impls::select_option_controller::util::*; +use crate::services::group::entities::GroupData; +use crate::services::group::{make_no_status_group, GeneratedGroupContext, GroupContext}; + +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize)] +pub struct SingleSelectGroupConfiguration { + pub hide_empty: bool, +} + +pub type SingleSelectOptionGroupContext = GroupContext; + +// SingleSelect +pub type SingleSelectGroupController = GenericGroupController< + SingleSelectGroupConfiguration, + SingleSelectTypeOption, + SingleSelectGroupGenerator, + SelectOptionCellDataParser, +>; + +impl GroupCustomize for SingleSelectGroupController { + type CellData = SelectOptionCellDataPB; + fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + cell_data + .select_options + .iter() + .any(|option| option.id == content) + } + + fn add_or_remove_row_when_cell_changed( + &mut self, + row: &Row, + cell_data: &Self::CellData, + ) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_status_groups(|group| { + if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row) { + changesets.push(changeset); + } + }); + changesets + } + + fn delete_row(&mut self, row: &Row, cell_data: &Self::CellData) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_status_groups(|group| { + if let Some(changeset) = remove_select_option_row(group, cell_data, row) { + changesets.push(changeset); + } + }); + changesets + } + + fn move_row( + &mut self, + _cell_data: &Self::CellData, + mut context: MoveGroupRowContext, + ) -> Vec { + let mut group_changeset = vec![]; + self.group_ctx.iter_mut_groups(|group| { + if let Some(changeset) = move_group_row(group, &mut context) { + group_changeset.push(changeset); + } + }); + group_changeset + } +} + +impl GroupController for SingleSelectGroupController { + fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + let group: Option<&mut GroupData> = self.group_ctx.get_mut_group(group_id); + match group { + None => {}, + Some(group) => { + let cell = insert_select_option_cell(vec![group.id.clone()], field); + cells.insert(field.id.clone(), cell); + }, + } + } + fn did_create_row(&mut self, row: &Row, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row.clone()) + } + } +} + +pub struct SingleSelectGroupGenerator(); +impl GroupGenerator for SingleSelectGroupGenerator { + type Context = SingleSelectOptionGroupContext; + type TypeOptionType = SingleSelectTypeOption; + fn generate_groups( + field: &Field, + _group_ctx: &Self::Context, + type_option: &Option, + ) -> GeneratedGroupContext { + let group_configs = match type_option { + None => vec![], + Some(type_option) => generate_select_option_groups(&field.id, &type_option.options), + }; + + GeneratedGroupContext { + no_status_group: Some(make_no_status_group(field)), + group_configs, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs new file mode 100644 index 0000000000..07a664711d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -0,0 +1,176 @@ +use collab_database::fields::Field; +use collab_database::rows::{Cell, Row}; + +use crate::entities::{ + FieldType, GroupRowsNotificationPB, InsertedRowPB, RowPB, SelectOptionCellDataPB, +}; +use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell, insert_url_cell}; +use crate::services::field::{SelectOption, CHECK}; +use crate::services::group::controller::MoveGroupRowContext; +use crate::services::group::{GeneratedGroupConfig, Group, GroupData}; + +pub fn add_or_remove_select_option_row( + group: &mut GroupData, + cell_data: &SelectOptionCellDataPB, + row: &Row, +) -> Option { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + if cell_data.select_options.is_empty() { + if group.contains_row(row.id) { + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } + } else { + cell_data.select_options.iter().for_each(|option| { + if option.id == group.id { + if !group.contains_row(row.id) { + changeset + .inserted_rows + .push(InsertedRowPB::new(RowPB::from(row))); + group.add_row(row.clone()); + } + } else if group.contains_row(row.id) { + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } + }); + } + + if changeset.is_empty() { + None + } else { + Some(changeset) + } +} + +pub fn remove_select_option_row( + group: &mut GroupData, + cell_data: &SelectOptionCellDataPB, + row: &Row, +) -> Option { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + cell_data.select_options.iter().for_each(|option| { + if option.id == group.id && group.contains_row(row.id) { + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } + }); + + if changeset.is_empty() { + None + } else { + Some(changeset) + } +} + +pub fn move_group_row( + group: &mut GroupData, + context: &mut MoveGroupRowContext, +) -> Option { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + let MoveGroupRowContext { + row, + row_changeset, + field, + to_group_id, + to_row_id, + } = context; + + let from_index = group.index_of_row(row.id); + let to_index = match to_row_id { + None => None, + Some(to_row_id) => group.index_of_row(*to_row_id), + }; + + // Remove the row in which group contains it + if let Some(from_index) = &from_index { + changeset.deleted_rows.push(row.id.into()); + tracing::debug!("Group:{} remove {} at {}", group.id, row.id, from_index); + group.remove_row(row.id); + } + + if group.id == *to_group_id { + let mut inserted_row = InsertedRowPB::new(RowPB::from(*row)); + match to_index { + None => { + changeset.inserted_rows.push(inserted_row); + tracing::debug!("Group:{} append row:{}", group.id, row.id); + group.add_row(row.clone()); + }, + Some(to_index) => { + if to_index < group.number_of_row() { + tracing::debug!("Group:{} insert {} at {} ", group.id, row.id, to_index); + inserted_row.index = Some(to_index as i32); + group.insert_row(to_index, (*row).clone()); + } else { + tracing::warn!("Move to index: {} is out of bounds", to_index); + tracing::debug!("Group:{} append row:{}", group.id, row.id); + group.add_row((*row).clone()); + } + changeset.inserted_rows.push(inserted_row); + }, + } + + // Update the corresponding row's cell content. + // If the from_index is none which means the row is not belong to this group before and + // it is moved from other groups. + if from_index.is_none() { + let cell = make_inserted_cell(&group.id, field); + if let Some(cell) = cell { + tracing::debug!( + "Update content of the cell in the row:{} to group:{}", + row.id, + group.id + ); + row_changeset + .cell_by_field_id + .insert(field.id.clone(), cell); + } + } + } + if changeset.is_empty() { + None + } else { + Some(changeset) + } +} + +pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option { + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::SingleSelect => { + let cell = insert_select_option_cell(vec![group_id.to_owned()], field); + Some(cell) + }, + FieldType::MultiSelect => { + let cell = insert_select_option_cell(vec![group_id.to_owned()], field); + Some(cell) + }, + FieldType::Checkbox => { + let cell = insert_checkbox_cell(group_id == CHECK, field); + Some(cell) + }, + FieldType::URL => { + let cell = insert_url_cell(group_id.to_owned(), field); + Some(cell) + }, + _ => { + tracing::warn!("Unknown field type: {:?}", field_type); + None + }, + } +} +pub fn generate_select_option_groups( + _field_id: &str, + options: &[SelectOption], +) -> Vec { + let groups = options + .iter() + .map(|option| GeneratedGroupConfig { + group: Group::new(option.id.clone(), option.name.clone()), + filter_content: option.id.clone(), + }) + .collect(); + + groups +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs new file mode 100644 index 0000000000..90f72a2429 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -0,0 +1,222 @@ +use collab_database::fields::Field; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use serde::{Deserialize, Serialize}; + +use flowy_error::FlowyResult; + +use crate::entities::{ + FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowPB, URLCellDataPB, +}; +use crate::services::cell::insert_url_cell; +use crate::services::field::{URLCellData, URLCellDataParser, URLTypeOption}; +use crate::services::group::action::GroupCustomize; +use crate::services::group::configuration::GroupContext; +use crate::services::group::controller::{ + GenericGroupController, GroupController, GroupGenerator, MoveGroupRowContext, +}; +use crate::services::group::{ + make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroupContext, Group, +}; + +#[derive(Default, Serialize, Deserialize)] +pub struct URLGroupConfiguration { + pub hide_empty: bool, +} + +pub type URLGroupController = GenericGroupController< + URLGroupConfiguration, + URLTypeOption, + URLGroupGenerator, + URLCellDataParser, +>; + +pub type URLGroupContext = GroupContext; + +impl GroupCustomize for URLGroupController { + type CellData = URLCellDataPB; + + fn placeholder_cell(&self) -> Option { + Some( + new_cell_builder(FieldType::URL) + .insert_str_value("data", "") + .build(), + ) + } + + fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + cell_data.content == content + } + + fn create_or_delete_group_when_cell_changed( + &mut self, + row: &Row, + _old_cell_data: Option<&Self::CellData>, + _cell_data: &Self::CellData, + ) -> FlowyResult<(Option, Option)> { + // Just return if the group with this url already exists + let mut inserted_group = None; + if self.group_ctx.get_group(&_cell_data.url).is_none() { + let cell_data: URLCellData = _cell_data.clone().into(); + let group = make_group_from_url_cell(&cell_data); + let mut new_group = self.group_ctx.add_new_group(group)?; + new_group.group.rows.push(RowPB::from(row)); + inserted_group = Some(new_group); + } + + // Delete the old url group if there are no rows in that group + let deleted_group = match _old_cell_data + .and_then(|old_cell_data| self.group_ctx.get_group(&old_cell_data.content)) + { + None => None, + Some((_, group)) => { + if group.rows.len() == 1 { + Some(group.clone()) + } else { + None + } + }, + }; + + let deleted_group = match deleted_group { + None => None, + Some(group) => { + self.group_ctx.delete_group(&group.id)?; + Some(GroupPB::from(group.clone())) + }, + }; + + Ok((inserted_group, deleted_group)) + } + + fn add_or_remove_row_when_cell_changed( + &mut self, + row: &Row, + cell_data: &Self::CellData, + ) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_status_groups(|group| { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + if group.id == cell_data.content { + if !group.contains_row(row.id) { + changeset + .inserted_rows + .push(InsertedRowPB::new(RowPB::from(row))); + group.add_row(row.clone()); + } + } else if group.contains_row(row.id) { + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn delete_row(&mut self, row: &Row, _cell_data: &Self::CellData) -> Vec { + let mut changesets = vec![]; + self.group_ctx.iter_mut_groups(|group| { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + if group.contains_row(row.id) { + changeset.deleted_rows.push(row.id.into()); + group.remove_row(row.id); + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn move_row( + &mut self, + _cell_data: &Self::CellData, + mut context: MoveGroupRowContext, + ) -> Vec { + let mut group_changeset = vec![]; + self.group_ctx.iter_mut_groups(|group| { + if let Some(changeset) = move_group_row(group, &mut context) { + group_changeset.push(changeset); + } + }); + group_changeset + } + + fn delete_group_when_move_row( + &mut self, + _row: &Row, + _cell_data: &Self::CellData, + ) -> Option { + let mut deleted_group = None; + if let Some((_, group)) = self.group_ctx.get_group(&_cell_data.content) { + if group.rows.len() == 1 { + deleted_group = Some(GroupPB::from(group.clone())); + } + } + if deleted_group.is_some() { + let _ = self + .group_ctx + .delete_group(&deleted_group.as_ref().unwrap().group_id); + } + deleted_group + } +} + +impl GroupController for URLGroupController { + fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + match self.group_ctx.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_, group)) => { + let cell = insert_url_cell(group.id.clone(), field); + cells.insert(field.id.clone(), cell); + }, + } + } + + fn did_create_row(&mut self, row: &Row, group_id: &str) { + if let Some(group) = self.group_ctx.get_mut_group(group_id) { + group.add_row(row.clone()) + } + } +} + +pub struct URLGroupGenerator(); +impl GroupGenerator for URLGroupGenerator { + type Context = URLGroupContext; + type TypeOptionType = URLTypeOption; + + fn generate_groups( + field: &Field, + group_ctx: &Self::Context, + _type_option: &Option, + ) -> GeneratedGroupContext { + // Read all the cells for the grouping field + let cells = futures::executor::block_on(group_ctx.get_all_cells()); + + // Generate the groups + let group_configs = cells + .into_iter() + .flat_map(|value| value.into_url_field_cell_data()) + .filter(|cell| !cell.data.is_empty()) + .map(|cell| GeneratedGroupConfig { + group: make_group_from_url_cell(&cell), + filter_content: cell.data, + }) + .collect(); + + let no_status_group = Some(make_no_status_group(field)); + GeneratedGroupContext { + no_status_group, + group_configs, + } + } +} + +fn make_group_from_url_cell(cell: &URLCellData) -> Group { + let group_id = cell.data.clone(); + let group_name = cell.data.clone(); + Group::new(group_id, group_name) +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs new file mode 100644 index 0000000000..8354cf2950 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -0,0 +1,191 @@ +use anyhow::bail; +use collab::core::any_map::AnyMapExtension; +use collab_database::database::gen_database_group_id; +use collab_database::rows::{Row, RowId}; +use collab_database::views::{GroupMap, GroupMapBuilder, GroupSettingBuilder, GroupSettingMap}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default)] +pub struct GroupSetting { + pub id: String, + pub field_id: String, + pub field_type: i64, + pub groups: Vec, + pub content: String, +} + +impl GroupSetting { + pub fn new(field_id: String, field_type: i64, content: String) -> Self { + Self { + id: gen_database_group_id(), + field_id, + field_type, + groups: vec![], + content, + } + } +} + +const GROUP_ID: &str = "id"; +const FIELD_ID: &str = "field_id"; +const FIELD_TYPE: &str = "ty"; +const GROUPS: &str = "groups"; +const CONTENT: &str = "content"; + +impl TryFrom for GroupSetting { + type Error = anyhow::Error; + + fn try_from(value: GroupSettingMap) -> Result { + match ( + value.get_str_value(GROUP_ID), + value.get_str_value(FIELD_ID), + value.get_i64_value(FIELD_TYPE), + ) { + (Some(id), Some(field_id), Some(field_type)) => { + let content = value.get_str_value(CONTENT).unwrap_or_default(); + let groups = value.try_get_array(GROUPS); + Ok(Self { + id, + field_id, + field_type, + groups, + content, + }) + }, + _ => { + bail!("Invalid group setting data") + }, + } + } +} + +impl From for GroupSettingMap { + fn from(setting: GroupSetting) -> Self { + GroupSettingBuilder::new() + .insert_str_value(GROUP_ID, setting.id) + .insert_str_value(FIELD_ID, setting.field_id) + .insert_i64_value(FIELD_TYPE, setting.field_type) + .insert_maps(GROUPS, setting.groups) + .insert_str_value(CONTENT, setting.content) + .build() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Group { + pub id: String, + pub name: String, + #[serde(default = "GROUP_REV_VISIBILITY")] + pub visible: bool, +} + +impl TryFrom for Group { + type Error = anyhow::Error; + + fn try_from(value: GroupMap) -> Result { + match value.get_str_value("id") { + None => bail!("Invalid group data"), + Some(id) => { + let name = value.get_str_value("name").unwrap_or_default(); + let visible = value.get_bool_value("visible").unwrap_or_default(); + Ok(Self { id, name, visible }) + }, + } + } +} + +impl From for GroupMap { + fn from(group: Group) -> Self { + GroupMapBuilder::new() + .insert_str_value("id", group.id) + .insert_str_value("name", group.name) + .insert_bool_value("visible", group.visible) + .build() + } +} + +const GROUP_REV_VISIBILITY: fn() -> bool = || true; + +impl Group { + pub fn new(id: String, name: String) -> Self { + Self { + id, + name, + visible: true, + } + } +} + +#[derive(Clone, Debug)] +pub struct GroupData { + pub id: String, + pub field_id: String, + pub name: String, + pub is_default: bool, + pub is_visible: bool, + pub(crate) rows: Vec, + + /// [filter_content] is used to determine which group the cell belongs to. + pub filter_content: String, +} + +impl GroupData { + pub fn new(id: String, field_id: String, name: String, filter_content: String) -> Self { + let is_default = id == field_id; + Self { + id, + field_id, + is_default, + is_visible: true, + name, + rows: vec![], + filter_content, + } + } + + pub fn contains_row(&self, row_id: RowId) -> bool { + self.rows.iter().any(|row| row.id == row_id) + } + + pub fn remove_row(&mut self, row_id: RowId) { + match self.rows.iter().position(|row| row.id == row_id) { + None => {}, + Some(pos) => { + self.rows.remove(pos); + }, + } + } + + pub fn add_row(&mut self, row: Row) { + match self.rows.iter().find(|r| r.id == row.id) { + None => { + self.rows.push(row); + }, + Some(_) => {}, + } + } + + pub fn insert_row(&mut self, index: usize, row: Row) { + if index < self.rows.len() { + self.rows.insert(index, row); + } else { + tracing::error!( + "Insert row index:{} beyond the bounds:{},", + index, + self.rows.len() + ); + } + } + + pub fn index_of_row(&self, row_id: RowId) -> Option { + self.rows.iter().position(|row| row.id == row_id) + } + + pub fn number_of_row(&self) -> usize { + self.rows.len() + } + + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_util.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_util.rs new file mode 100644 index 0000000000..6cbbfd89c4 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_util.rs @@ -0,0 +1,161 @@ +use crate::entities::FieldType; +use crate::services::group::configuration::GroupSettingReader; +use crate::services::group::controller::GroupController; +use crate::services::group::{ + CheckboxGroupContext, CheckboxGroupController, DefaultGroupController, Group, GroupSetting, + GroupSettingWriter, MultiSelectGroupController, MultiSelectOptionGroupContext, + SingleSelectGroupController, SingleSelectOptionGroupContext, URLGroupContext, URLGroupController, +}; +use collab_database::fields::Field; +use collab_database::rows::Row; +use collab_database::views::DatabaseLayout; + +use flowy_error::FlowyResult; +use std::sync::Arc; + +/// Returns a group controller. +/// +/// Each view can be grouped by one field, each field has its own group controller. +/// # Arguments +/// +/// * `view_id`: the id of the view +/// * `grouping_field_rev`: the grouping field +/// * `row_revs`: the rows will be separated into different groups +/// * `configuration_reader`: a reader used to read the group configuration from disk +/// * `configuration_writer`: as writer used to write the group configuration to disk +/// +#[tracing::instrument( + level = "debug", + skip_all, + fields(grouping_field_id=%grouping_field.id, grouping_field_type) + err +)] +pub async fn make_group_controller( + view_id: String, + grouping_field: Arc, + rows: Vec>, + setting_reader: R, + setting_writer: W, +) -> FlowyResult> +where + R: GroupSettingReader, + W: GroupSettingWriter, +{ + let grouping_field_type = FieldType::from(grouping_field.field_type); + tracing::Span::current().record("grouping_field_type", &grouping_field_type.default_name()); + + let mut group_controller: Box; + let configuration_reader = Arc::new(setting_reader); + let configuration_writer = Arc::new(setting_writer); + + match grouping_field_type { + FieldType::SingleSelect => { + let configuration = SingleSelectOptionGroupContext::new( + view_id, + grouping_field.clone(), + configuration_reader, + configuration_writer, + ) + .await?; + let controller = SingleSelectGroupController::new(&grouping_field, configuration).await?; + group_controller = Box::new(controller); + }, + FieldType::MultiSelect => { + let configuration = MultiSelectOptionGroupContext::new( + view_id, + grouping_field.clone(), + configuration_reader, + configuration_writer, + ) + .await?; + let controller = MultiSelectGroupController::new(&grouping_field, configuration).await?; + group_controller = Box::new(controller); + }, + FieldType::Checkbox => { + let configuration = CheckboxGroupContext::new( + view_id, + grouping_field.clone(), + configuration_reader, + configuration_writer, + ) + .await?; + let controller = CheckboxGroupController::new(&grouping_field, configuration).await?; + group_controller = Box::new(controller); + }, + FieldType::URL => { + let configuration = URLGroupContext::new( + view_id, + grouping_field.clone(), + configuration_reader, + configuration_writer, + ) + .await?; + let controller = URLGroupController::new(&grouping_field, configuration).await?; + group_controller = Box::new(controller); + }, + _ => { + group_controller = Box::new(DefaultGroupController::new(&grouping_field)); + }, + } + + // Separates the rows into different groups + let rows = rows.iter().map(|row| row.as_ref()).collect::>(); + group_controller.fill_groups(rows.as_slice(), &grouping_field)?; + Ok(group_controller) +} + +#[tracing::instrument(level = "debug", skip_all)] +pub fn find_new_grouping_field( + fields: &[Arc], + _layout: &DatabaseLayout, +) -> Option> { + let mut groupable_field_revs = fields + .iter() + .flat_map(|field_rev| { + let field_type = FieldType::from(field_rev.field_type); + match field_type.can_be_group() { + true => Some(field_rev.clone()), + false => None, + } + }) + .collect::>>(); + + if groupable_field_revs.is_empty() { + // If there is not groupable fields then we use the primary field. + fields + .iter() + .find(|field_rev| field_rev.is_primary) + .cloned() + } else { + Some(groupable_field_revs.remove(0)) + } +} + +/// Returns a `default` group configuration for the [Field] +/// +/// # Arguments +/// +/// * `field`: making the group configuration for the field +/// +pub fn default_group_setting(field: &Field) -> GroupSetting { + let field_id = field.id.clone(); + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::RichText => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::Number => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::DateTime => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::SingleSelect => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::MultiSelect => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::Checklist => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::Checkbox => GroupSetting::new(field_id, field.field_type, "".to_owned()), + FieldType::URL => GroupSetting::new(field_id, field.field_type, "".to_owned()), + } +} + +pub fn make_no_status_group(field: &Field) -> Group { + Group { + id: field.id.clone(), + name: format!("No {}", field.name), + visible: true, + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs new file mode 100644 index 0000000000..b73ac511b6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs @@ -0,0 +1,12 @@ +mod action; +mod configuration; +mod controller; +mod controller_impls; +mod entities; +mod group_util; + +pub(crate) use configuration::*; +pub(crate) use controller::*; +pub(crate) use controller_impls::*; +pub(crate) use entities::*; +pub(crate) use group_util::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/mod.rs b/frontend/rust-lib/flowy-database2/src/services/mod.rs new file mode 100644 index 0000000000..28e4810696 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/mod.rs @@ -0,0 +1,8 @@ +pub mod cell; +pub mod database; +pub mod database_view; +pub mod field; +pub mod filter; +pub mod group; +pub mod setting; +pub mod sort; diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs new file mode 100644 index 0000000000..808d2f7a75 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -0,0 +1,91 @@ +use collab::core::any_map::AnyMapExtension; +use collab_database::views::{LayoutSetting, LayoutSettingBuilder}; +use serde::{Deserialize, Serialize}; +use serde_repr::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CalendarLayoutSetting { + pub layout_ty: CalendarLayout, + pub first_day_of_week: i32, + pub show_weekends: bool, + pub show_week_numbers: bool, + pub field_id: String, +} + +impl From for CalendarLayoutSetting { + fn from(setting: LayoutSetting) -> Self { + let layout_ty = setting + .get_i64_value("layout_ty") + .map(CalendarLayout::from) + .unwrap_or_default(); + let first_day_of_week = setting + .get_i64_value("first_day_of_week") + .unwrap_or(DEFAULT_FIRST_DAY_OF_WEEK as i64) as i32; + let show_weekends = setting.get_bool_value("show_weekends").unwrap_or_default(); + let show_week_numbers = setting + .get_bool_value("show_week_numbers") + .unwrap_or_default(); + let field_id = setting.get_str_value("field_id").unwrap_or_default(); + Self { + layout_ty, + first_day_of_week, + show_weekends, + show_week_numbers, + field_id, + } + } +} + +impl From for LayoutSetting { + fn from(setting: CalendarLayoutSetting) -> Self { + LayoutSettingBuilder::new() + .insert_i64_value("layout_ty", setting.layout_ty.value()) + .insert_i64_value("first_day_of_week", setting.first_day_of_week as i64) + .insert_bool_value("show_week_numbers", setting.show_week_numbers) + .insert_bool_value("show_weekends", setting.show_weekends) + .insert_str_value("field_id", setting.field_id) + .build() + } +} + +impl CalendarLayoutSetting { + pub fn new(field_id: String) -> Self { + CalendarLayoutSetting { + layout_ty: CalendarLayout::default(), + first_day_of_week: DEFAULT_FIRST_DAY_OF_WEEK, + show_weekends: DEFAULT_SHOW_WEEKENDS, + show_week_numbers: DEFAULT_SHOW_WEEK_NUMBERS, + field_id, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum CalendarLayout { + #[default] + Month = 0, + Week = 1, + Day = 2, +} + +impl From for CalendarLayout { + fn from(value: i64) -> Self { + match value { + 0 => CalendarLayout::Month, + 1 => CalendarLayout::Week, + 2 => CalendarLayout::Day, + _ => CalendarLayout::Month, + } + } +} + +impl CalendarLayout { + pub fn value(&self) -> i64 { + *self as i64 + } +} + +pub const DEFAULT_FIRST_DAY_OF_WEEK: i32 = 0; +pub const DEFAULT_SHOW_WEEKENDS: bool = true; +pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true; diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/mod.rs b/frontend/rust-lib/flowy-database2/src/services/setting/mod.rs new file mode 100644 index 0000000000..127f8ee599 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/setting/mod.rs @@ -0,0 +1,3 @@ +mod entities; + +pub use entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs new file mode 100644 index 0000000000..c67ba85881 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -0,0 +1,308 @@ +use std::cmp::Ordering; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::{Cell, Row, RowId}; +use rayon::prelude::ParallelSliceMut; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use flowy_error::FlowyResult; +use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; +use lib_infra::future::Fut; + +use crate::entities::FieldType; +use crate::entities::SortChangesetNotificationPB; +use crate::services::cell::CellCache; +use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; +use crate::services::field::{default_order, TypeOptionCellExt}; +use crate::services::sort::{ + ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition, +}; + +pub trait SortDelegate: Send + Sync { + fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>>; + /// Returns all the rows after applying grid's filter + fn get_rows(&self, view_id: &str) -> Fut>>; + fn get_field(&self, field_id: &str) -> Fut>>; + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; +} + +pub struct SortController { + view_id: String, + handler_id: String, + delegate: Box, + task_scheduler: Arc>, + sorts: Vec>, + cell_cache: CellCache, + row_index_cache: HashMap, + notifier: DatabaseViewChangedNotifier, +} + +impl Drop for SortController { + fn drop(&mut self) { + tracing::trace!("Drop {}", std::any::type_name::()); + } +} + +impl SortController { + pub fn new( + view_id: &str, + handler_id: &str, + sorts: Vec>, + delegate: T, + task_scheduler: Arc>, + cell_cache: CellCache, + notifier: DatabaseViewChangedNotifier, + ) -> Self + where + T: SortDelegate + 'static, + { + Self { + view_id: view_id.to_string(), + handler_id: handler_id.to_string(), + delegate: Box::new(delegate), + task_scheduler, + sorts, + cell_cache, + row_index_cache: Default::default(), + notifier, + } + } + + pub async fn close(&self) { + if let Ok(mut task_scheduler) = self.task_scheduler.try_write() { + task_scheduler.unregister_handler(&self.handler_id).await; + } else { + tracing::error!("Try to get the lock of task_scheduler failed"); + } + } + + pub async fn did_receive_row_changed(&self, row_id: RowId) { + let task_type = SortEvent::RowDidChanged(row_id); + self.gen_task(task_type, QualityOfService::Background).await; + } + + #[tracing::instrument(name = "process_sort_task", level = "trace", skip_all, err)] + pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> { + let event_type = SortEvent::from_str(predicate).unwrap(); + let mut rows = self.delegate.get_rows(&self.view_id).await; + match event_type { + SortEvent::SortDidChanged => { + self.sort_rows(&mut rows).await; + let row_orders = rows + .iter() + .map(|row| row.id.to_string()) + .collect::>(); + + let notification = ReorderAllRowsResult { + view_id: self.view_id.clone(), + row_orders, + }; + + let _ = self + .notifier + .send(DatabaseViewChanged::ReorderAllRowsNotification( + notification, + )); + }, + SortEvent::RowDidChanged(row_id) => { + let old_row_index = self.row_index_cache.get(&row_id).cloned(); + self.sort_rows(&mut rows).await; + let new_row_index = self.row_index_cache.get(&row_id).cloned(); + match (old_row_index, new_row_index) { + (Some(old_row_index), Some(new_row_index)) => { + if old_row_index == new_row_index { + return Ok(()); + } + let notification = ReorderSingleRowResult { + row_id: row_id.into(), + view_id: self.view_id.clone(), + old_index: old_row_index, + new_index: new_row_index, + }; + let _ = self + .notifier + .send(DatabaseViewChanged::ReorderSingleRowNotification( + notification, + )); + }, + _ => tracing::trace!("The row index cache is outdated"), + } + }, + } + Ok(()) + } + + #[tracing::instrument(name = "schedule_sort_task", level = "trace", skip(self))] + async fn gen_task(&self, task_type: SortEvent, qos: QualityOfService) { + let task_id = self.task_scheduler.read().await.next_task_id(); + let task = Task::new( + &self.handler_id, + task_id, + TaskContent::Text(task_type.to_string()), + qos, + ); + self.task_scheduler.write().await.add_task(task); + } + + pub async fn sort_rows(&mut self, rows: &mut Vec>) { + if self.sorts.is_empty() { + return; + } + + let field_revs = self.delegate.get_fields(&self.view_id, None).await; + for sort in self.sorts.iter() { + rows.par_sort_by(|left, right| cmp_row(left, right, sort, &field_revs, &self.cell_cache)); + } + rows.iter().enumerate().for_each(|(index, row)| { + self.row_index_cache.insert(row.id, index); + }); + } + + pub async fn delete_all_sorts(&mut self) { + self.sorts.clear(); + self + .gen_task(SortEvent::SortDidChanged, QualityOfService::Background) + .await; + } + + pub async fn did_update_view_field_type_option(&self, _field_rev: &Field) { + // + } + + #[tracing::instrument(level = "trace", skip(self))] + pub async fn did_receive_changes( + &mut self, + changeset: SortChangeset, + ) -> SortChangesetNotificationPB { + let mut notification = SortChangesetNotificationPB::new(self.view_id.clone()); + if let Some(insert_sort) = changeset.insert_sort { + if let Some(sort) = self + .delegate + .get_sort(&self.view_id, &insert_sort.sort_id) + .await + { + notification.insert_sorts.push(sort.as_ref().into()); + self.sorts.push(sort); + } + } + + if let Some(delete_sort_type) = changeset.delete_sort { + if let Some(index) = self + .sorts + .iter() + .position(|sort| sort.id == delete_sort_type.sort_id) + { + let sort = self.sorts.remove(index); + notification.delete_sorts.push(sort.as_ref().into()); + } + } + + if let Some(update_sort) = changeset.update_sort { + if let Some(updated_sort) = self + .delegate + .get_sort(&self.view_id, &update_sort.sort_id) + .await + { + notification.update_sorts.push(updated_sort.as_ref().into()); + if let Some(index) = self + .sorts + .iter() + .position(|sort| sort.id == updated_sort.id) + { + self.sorts[index] = updated_sort; + } + } + } + + if !notification.is_empty() { + self + .gen_task(SortEvent::SortDidChanged, QualityOfService::UserInteractive) + .await; + } + tracing::trace!("sort notification: {:?}", notification); + notification + } +} + +fn cmp_row( + left: &Arc, + right: &Arc, + sort: &Arc, + field_revs: &[Arc], + cell_data_cache: &CellCache, +) -> Ordering { + let order = match ( + left.cells.get(&sort.field_id), + right.cells.get(&sort.field_id), + ) { + (Some(left_cell), Some(right_cell)) => { + let field_type = sort.field_type.clone(); + match field_revs + .iter() + .find(|field_rev| field_rev.id == sort.field_id) + { + None => default_order(), + Some(field_rev) => cmp_cell( + left_cell, + right_cell, + field_rev, + field_type, + cell_data_cache, + ), + } + }, + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + _ => default_order(), + }; + + // The order is calculated by Ascending. So reverse the order if the SortCondition is descending. + match sort.condition { + SortCondition::Ascending => order, + SortCondition::Descending => order.reverse(), + } +} + +fn cmp_cell( + left_cell: &Cell, + right_cell: &Cell, + field: &Arc, + field_type: FieldType, + cell_data_cache: &CellCache, +) -> Ordering { + match TypeOptionCellExt::new_with_cell_data_cache(field.as_ref(), Some(cell_data_cache.clone())) + .get_type_option_cell_data_handler(&field_type) + { + None => default_order(), + Some(handler) => { + let cal_order = || { + let order = handler.handle_cell_compare(left_cell, right_cell, field.as_ref()); + Option::::Some(order) + }; + + cal_order().unwrap_or_else(default_order) + }, + } +} +#[derive(Serialize, Deserialize, Clone, Debug)] +enum SortEvent { + SortDidChanged, + RowDidChanged(RowId), +} + +impl ToString for SortEvent { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl FromStr for SortEvent { + type Err = serde_json::Error; + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs new file mode 100644 index 0000000000..78f2cfa29d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -0,0 +1,173 @@ +use anyhow::bail; +use collab::core::any_map::AnyMapExtension; +use collab_database::views::{SortMap, SortMapBuilder}; + +use crate::entities::{DeleteSortParams, FieldType}; + +#[derive(Debug, Clone)] +pub struct Sort { + pub id: String, + pub field_id: String, + pub field_type: FieldType, + pub condition: SortCondition, +} + +const SORT_ID: &str = "id"; +const FIELD_ID: &str = "field_id"; +const FIELD_TYPE: &str = "ty"; +const SORT_CONDITION: &str = "condition"; + +impl TryFrom for Sort { + type Error = anyhow::Error; + + fn try_from(value: SortMap) -> Result { + match ( + value.get_str_value(SORT_ID), + value.get_str_value(FIELD_ID), + value.get_i64_value(FIELD_TYPE).map(FieldType::from), + ) { + (Some(id), Some(field_id), Some(field_type)) => { + let condition = + SortCondition::try_from(value.get_i64_value(SORT_CONDITION).unwrap_or_default()) + .unwrap_or_default(); + Ok(Self { + id, + field_id, + field_type, + condition, + }) + }, + _ => { + bail!("Invalid sort data") + }, + } + } +} + +impl From for SortMap { + fn from(data: Sort) -> Self { + SortMapBuilder::new() + .insert_str_value(SORT_ID, data.id) + .insert_str_value(FIELD_ID, data.field_id) + .insert_i64_value(FIELD_TYPE, data.field_type.into()) + .insert_i64_value(SORT_CONDITION, data.condition.value()) + .build() + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +pub enum SortCondition { + Ascending = 0, + Descending = 1, +} + +impl SortCondition { + pub fn value(&self) -> i64 { + *self as i64 + } +} + +impl Default for SortCondition { + fn default() -> Self { + Self::Ascending + } +} + +impl From for SortCondition { + fn from(value: i64) -> Self { + match value { + 0 => SortCondition::Ascending, + 1 => SortCondition::Descending, + _ => SortCondition::Ascending, + } + } +} + +#[derive(Hash, Eq, PartialEq, Debug, Clone)] +pub struct SortType { + pub sort_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl From<&Sort> for SortType { + fn from(data: &Sort) -> Self { + Self { + sort_id: data.id.clone(), + field_id: data.field_id.clone(), + field_type: data.field_type.clone(), + } + } +} + +#[derive(Clone)] +pub struct ReorderAllRowsResult { + pub view_id: String, + pub row_orders: Vec, +} + +impl ReorderAllRowsResult { + pub fn new(view_id: String, row_orders: Vec) -> Self { + Self { + view_id, + row_orders, + } + } +} + +#[derive(Clone)] +pub struct ReorderSingleRowResult { + pub view_id: String, + pub row_id: i64, + pub old_index: usize, + pub new_index: usize, +} + +#[derive(Debug)] +pub struct SortChangeset { + pub(crate) insert_sort: Option, + pub(crate) update_sort: Option, + pub(crate) delete_sort: Option, +} + +impl SortChangeset { + pub fn from_insert(sort: SortType) -> Self { + Self { + insert_sort: Some(sort), + update_sort: None, + delete_sort: None, + } + } + + pub fn from_update(sort: SortType) -> Self { + Self { + insert_sort: None, + update_sort: Some(sort), + delete_sort: None, + } + } + + pub fn from_delete(deleted_sort: DeletedSortType) -> Self { + Self { + insert_sort: None, + update_sort: None, + delete_sort: Some(deleted_sort), + } + } +} + +#[derive(Debug)] +pub struct DeletedSortType { + pub sort_type: SortType, + pub sort_id: String, +} + +impl std::convert::From for DeletedSortType { + fn from(params: DeleteSortParams) -> Self { + Self { + sort_type: params.sort_type, + sort_id: params.sort_id, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/mod.rs b/frontend/rust-lib/flowy-database2/src/services/sort/mod.rs new file mode 100644 index 0000000000..89f10043b0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/sort/mod.rs @@ -0,0 +1,7 @@ +mod controller; +mod entities; +mod task; + +pub use controller::*; +pub use entities::*; +pub use task::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/task.rs b/frontend/rust-lib/flowy-database2/src/services/sort/task.rs new file mode 100644 index 0000000000..9bac020e8d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/sort/task.rs @@ -0,0 +1,45 @@ +use crate::services::sort::SortController; +use flowy_task::{TaskContent, TaskHandler}; +use lib_infra::future::BoxResultFuture; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct SortTaskHandler { + handler_id: String, + #[allow(dead_code)] + sort_controller: Arc>, +} + +impl SortTaskHandler { + pub fn new(handler_id: String, sort_controller: Arc>) -> Self { + Self { + handler_id, + sort_controller, + } + } +} + +impl TaskHandler for SortTaskHandler { + fn handler_id(&self) -> &str { + &self.handler_id + } + + fn handler_name(&self) -> &str { + "SortTaskHandler" + } + + fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> { + let sort_controller = self.sort_controller.clone(); + Box::pin(async move { + if let TaskContent::Text(predicate) = content { + sort_controller + .write() + .await + .process(&predicate) + .await + .map_err(anyhow::Error::from)?; + } + Ok(()) + }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/template.rs b/frontend/rust-lib/flowy-database2/src/template.rs new file mode 100644 index 0000000000..35b09e69cc --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -0,0 +1,137 @@ +use collab_database::database::{gen_database_id, gen_row_id}; +use collab_database::rows::CreateRowParams; +use collab_database::views::{CreateDatabaseParams, DatabaseLayout, LayoutSettings}; + +use crate::entities::FieldType; +use crate::services::cell::{insert_select_option_cell, insert_text_cell}; +use crate::services::field::{ + FieldBuilder, SelectOption, SelectOptionColor, SingleSelectTypeOption, +}; +use crate::services::setting::CalendarLayoutSetting; + +pub fn make_default_grid(view_id: &str, name: &str) -> CreateDatabaseParams { + let text_field = FieldBuilder::from_field_type(FieldType::RichText) + .name("Name") + .visibility(true) + .primary(true) + .build(); + + let single_select = FieldBuilder::from_field_type(FieldType::SingleSelect) + .name("Type") + .visibility(true) + .build(); + + let checkbox_field = FieldBuilder::from_field_type(FieldType::Checkbox) + .name("Done") + .visibility(true) + .build(); + + CreateDatabaseParams { + database_id: gen_database_id(), + view_id: view_id.to_string(), + name: name.to_string(), + layout: DatabaseLayout::Grid, + layout_settings: Default::default(), + filters: vec![], + groups: vec![], + sorts: vec![], + created_rows: vec![ + CreateRowParams::new(gen_row_id()), + CreateRowParams::new(gen_row_id()), + CreateRowParams::new(gen_row_id()), + ], + fields: vec![text_field, single_select, checkbox_field], + } +} + +pub fn make_default_board(view_id: &str, name: &str) -> CreateDatabaseParams { + // text + let text_field = FieldBuilder::from_field_type(FieldType::RichText) + .name("Description") + .visibility(true) + .primary(true) + .build(); + let text_field_id = text_field.id.clone(); + + // single select + let to_do_option = SelectOption::with_color("To Do", SelectOptionColor::Purple); + let doing_option = SelectOption::with_color("Doing", SelectOptionColor::Orange); + let done_option = SelectOption::with_color("Done", SelectOptionColor::Yellow); + let mut single_select_type_option = SingleSelectTypeOption::default(); + single_select_type_option + .options + .extend(vec![to_do_option.clone(), doing_option, done_option]); + let single_select = FieldBuilder::new(FieldType::SingleSelect, single_select_type_option) + .name("Status") + .visibility(true) + .build(); + let single_select_field_id = single_select.id.clone(); + + let mut rows = vec![]; + for i in 0..3 { + let mut row = CreateRowParams::new(gen_row_id()); + row.cells.insert( + single_select_field_id.clone(), + insert_select_option_cell(vec![to_do_option.id.clone()], &single_select), + ); + row.cells.insert( + text_field_id.clone(), + insert_text_cell(format!("Card {}", i + 1), &text_field), + ); + rows.push(row); + } + + CreateDatabaseParams { + database_id: gen_database_id(), + view_id: view_id.to_string(), + name: name.to_string(), + layout: DatabaseLayout::Board, + layout_settings: Default::default(), + filters: vec![], + groups: vec![], + sorts: vec![], + created_rows: rows, + fields: vec![text_field, single_select], + } +} + +pub fn make_default_calendar(view_id: &str, name: &str) -> CreateDatabaseParams { + // text + let text_field = FieldBuilder::from_field_type(FieldType::RichText) + .name("Title") + .visibility(true) + .primary(true) + .build(); + + // date + let date_field = FieldBuilder::from_field_type(FieldType::DateTime) + .name("Date") + .visibility(true) + .build(); + let date_field_id = date_field.id.clone(); + + // multi select + let multi_select_field = FieldBuilder::from_field_type(FieldType::MultiSelect) + .name("Tags") + .visibility(true) + .build(); + + let mut layout_settings = LayoutSettings::default(); + layout_settings.insert( + DatabaseLayout::Calendar, + CalendarLayoutSetting::new(date_field_id).into(), + ); + + CreateDatabaseParams { + database_id: gen_database_id(), + view_id: view_id.to_string(), + name: name.to_string(), + layout: DatabaseLayout::Calendar, + layout_settings, + filters: vec![], + groups: vec![], + sorts: vec![], + created_rows: vec![], + fields: vec![text_field, date_field, multi_select_field], + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/mod.rs new file mode 100644 index 0000000000..63d424afaf --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/mod.rs @@ -0,0 +1,2 @@ +mod script; +mod test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs new file mode 100644 index 0000000000..a8c5197fe2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs @@ -0,0 +1,72 @@ +use collab_database::rows::RowId; + +use flowy_database2::entities::CellChangesetPB; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum CellScript { + UpdateCell { + changeset: CellChangesetPB, + is_err: bool, + }, +} + +pub struct DatabaseCellTest { + inner: DatabaseEditorTest, +} + +impl DatabaseCellTest { + pub async fn new() -> Self { + let inner = DatabaseEditorTest::new_grid().await; + Self { inner } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: CellScript) { + // let grid_manager = self.sdk.grid_manager.clone(); + // let pool = self.sdk.user_session.db_pool().unwrap(); + // let rev_manager = self.editor.rev_manager(); + // let _cache = rev_manager.revision_cache().await; + + match script { + CellScript::UpdateCell { + changeset, + is_err: _, + } => { + self + .editor + .update_cell_with_changeset( + &self.view_id, + RowId::from(changeset.row_id), + &changeset.field_id, + changeset.cell_changeset, + ) + .await; + }, // CellScript::AssertGridRevisionPad => { + // sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await; + // let mut grid_rev_manager = grid_manager.make_grid_rev_manager(&self.grid_id, pool.clone()).unwrap(); + // let grid_pad = grid_rev_manager.load::(None).await.unwrap(); + // println!("{}", grid_pad.delta_str()); + // } + } + } +} + +impl std::ops::Deref for DatabaseCellTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for DatabaseCellTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs new file mode 100644 index 0000000000..3976d01883 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs @@ -0,0 +1,105 @@ +use flowy_database2::entities::{CellChangesetPB, FieldType}; +use flowy_database2::services::cell::ToCellChangeset; +use flowy_database2::services::field::{ + ChecklistTypeOption, MultiSelectTypeOption, SelectOptionCellChangeset, SingleSelectTypeOption, + StrCellData, URLCellData, +}; + +use crate::database::cell_test::script::CellScript::UpdateCell; +use crate::database::cell_test::script::DatabaseCellTest; +use crate::database::field_test::util::make_date_cell_string; + +#[tokio::test] +async fn grid_cell_update() { + let mut test = DatabaseCellTest::new().await; + let fields = test.get_fields(); + let rows = &test.rows; + + let mut scripts = vec![]; + for (_, row) in rows.iter().enumerate() { + for field in &fields { + let field_type = FieldType::from(field.field_type); + let cell_changeset = match field_type { + FieldType::RichText => "".to_string(), + FieldType::Number => "123".to_string(), + FieldType::DateTime => make_date_cell_string("123"), + FieldType::SingleSelect => { + let type_option = field + .get_type_option::(field.field_type) + .unwrap(); + SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id) + .to_cell_changeset_str() + }, + FieldType::MultiSelect => { + let type_option = field + .get_type_option::(field.field_type) + .unwrap(); + SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id) + .to_cell_changeset_str() + }, + FieldType::Checklist => { + let type_option = field + .get_type_option::(field.field_type) + .unwrap(); + SelectOptionCellChangeset::from_insert_option_id(&type_option.options.first().unwrap().id) + .to_cell_changeset_str() + }, + FieldType::Checkbox => "1".to_string(), + FieldType::URL => "1".to_string(), + }; + + scripts.push(UpdateCell { + changeset: CellChangesetPB { + view_id: test.view_id.clone(), + row_id: row.id.into(), + field_id: field.id.clone(), + cell_changeset, + }, + is_err: false, + }); + } + } + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn text_cell_date_test() { + let test = DatabaseCellTest::new().await; + let text_field = test.get_first_field(FieldType::RichText); + + let cells = test + .editor + .get_cells_for_field(&test.view_id, &text_field.id) + .await; + + for (i, cell) in cells.into_iter().enumerate() { + let text = StrCellData::from(cell.as_ref()); + match i { + 0 => assert_eq!(text.as_str(), "A"), + 1 => assert_eq!(text.as_str(), ""), + 2 => assert_eq!(text.as_str(), "C"), + 3 => assert_eq!(text.as_str(), "DA"), + 4 => assert_eq!(text.as_str(), "AE"), + 5 => assert_eq!(text.as_str(), "AE"), + _ => {}, + } + } +} + +#[tokio::test] +async fn url_cell_date_test() { + let test = DatabaseCellTest::new().await; + let url_field = test.get_first_field(FieldType::URL); + let cells = test + .editor + .get_cells_for_field(&test.view_id, &url_field.id) + .await; + + for (i, cell) in cells.into_iter().enumerate() { + let cell = URLCellData::from(cell.as_ref()); + if i == 0 { + assert_eq!(cell.url.as_str(), "https://www.appflowy.io/"); + } + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs new file mode 100644 index 0000000000..ce7e025364 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -0,0 +1,364 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use collab_database::fields::Field; +use collab_database::rows::{CreateRowParams, Row, RowId}; +use strum::EnumCount; + +use flowy_database2::entities::{DatabaseLayoutPB, FieldType, FilterPB, RowPB}; +use flowy_database2::services::cell::{CellBuilder, ToCellChangeset}; +use flowy_database2::services::database::DatabaseEditor; +use flowy_database2::services::field::{ + CheckboxTypeOption, ChecklistTypeOption, DateCellChangeset, MultiSelectTypeOption, SelectOption, + SelectOptionCellChangeset, SingleSelectTypeOption, +}; +use flowy_test::helper::ViewTest; +use flowy_test::FlowySDKTest; + +use crate::database::mock_data::{make_test_board, make_test_calendar, make_test_grid}; + +pub struct DatabaseEditorTest { + pub sdk: FlowySDKTest, + pub app_id: String, + pub view_id: String, + pub editor: Arc, + pub fields: Vec>, + pub rows: Vec>, + pub field_count: usize, + pub row_by_row_id: HashMap, +} + +impl DatabaseEditorTest { + pub async fn new_grid() -> Self { + Self::new(DatabaseLayoutPB::Grid).await + } + + pub async fn new_board() -> Self { + Self::new(DatabaseLayoutPB::Board).await + } + + pub async fn new_calendar() -> Self { + Self::new(DatabaseLayoutPB::Calendar).await + } + + pub async fn new(layout: DatabaseLayoutPB) -> Self { + let sdk = FlowySDKTest::default(); + let _ = sdk.init_user().await; + let test = match layout { + DatabaseLayoutPB::Grid => { + let params = make_test_grid(); + ViewTest::new_grid_view(&sdk, params.to_json_bytes().unwrap()).await + }, + DatabaseLayoutPB::Board => { + let data = make_test_board(); + ViewTest::new_board_view(&sdk, data.to_json_bytes().unwrap()).await + }, + DatabaseLayoutPB::Calendar => { + let data = make_test_calendar(); + ViewTest::new_calendar_view(&sdk, data.to_json_bytes().unwrap()).await + }, + }; + + let editor = sdk + .database_manager + .get_database(&test.child_view.id) + .await + .unwrap(); + let fields = editor + .get_fields(&test.child_view.id, None) + .into_iter() + .map(Arc::new) + .collect(); + let rows = editor + .get_rows(&test.child_view.id) + .await + .unwrap() + .into_iter() + .collect(); + + let view_id = test.child_view.id; + let app_id = test.parent_view.id; + Self { + sdk, + app_id, + view_id, + editor, + fields, + rows, + field_count: FieldType::COUNT, + row_by_row_id: HashMap::default(), + } + } + + pub async fn database_filters(&self) -> Vec { + self.editor.get_all_filters(&self.view_id).await.items + } + + pub async fn get_rows(&self) -> Vec> { + self.editor.get_rows(&self.view_id).await.unwrap() + } + + pub fn get_field(&self, field_id: &str, field_type: FieldType) -> Field { + self + .editor + .get_fields(&self.view_id, None) + .into_iter() + .filter(|field| { + let t_field_type = FieldType::from(field.field_type); + field.id == field_id && t_field_type == field_type + }) + .collect::>() + .pop() + .unwrap() + } + + /// returns the first `Field` in the build-in test grid. + /// Not support duplicate `FieldType` in test grid yet. + pub fn get_first_field(&self, field_type: FieldType) -> Field { + self + .editor + .get_fields(&self.view_id, None) + .into_iter() + .filter(|field| { + let t_field_type = FieldType::from(field.field_type); + t_field_type == field_type + }) + .collect::>() + .pop() + .unwrap() + } + + pub fn get_fields(&self) -> Vec { + self.editor.get_fields(&self.view_id, None) + } + + pub fn get_multi_select_type_option(&self, field_id: &str) -> Vec { + let field_type = FieldType::MultiSelect; + let field = self.get_field(field_id, field_type.clone()); + let type_option = field + .get_type_option::(field_type) + .unwrap(); + type_option.options + } + + pub fn get_single_select_type_option(&self, field_id: &str) -> SingleSelectTypeOption { + let field_type = FieldType::SingleSelect; + let field = self.get_field(field_id, field_type.clone()); + field + .get_type_option::(field_type) + .unwrap() + } + + #[allow(dead_code)] + pub fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOption { + let field_type = FieldType::Checklist; + let field = self.get_field(field_id, field_type.clone()); + field + .get_type_option::(field_type) + .unwrap() + } + + #[allow(dead_code)] + pub fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOption { + let field_type = FieldType::Checkbox; + let field = self.get_field(field_id, field_type.clone()); + field + .get_type_option::(field_type) + .unwrap() + } + + pub async fn update_cell( + &mut self, + field_id: &str, + row_id: RowId, + cell_changeset: T, + ) { + let field = self + .editor + .get_fields(&self.view_id, None) + .into_iter() + .find(|field| field.id == field_id) + .unwrap(); + + self + .editor + .update_cell_with_changeset(&self.view_id, row_id, &field.id, cell_changeset) + .await; + } + + pub(crate) async fn update_text_cell(&mut self, row_id: RowId, content: &str) { + let field = self + .editor + .get_fields(&self.view_id, None) + .iter() + .find(|field| { + let field_type = FieldType::from(field.field_type); + field_type == FieldType::RichText + }) + .unwrap() + .clone(); + + self + .update_cell(&field.id, row_id, content.to_string()) + .await; + } + + pub(crate) async fn update_single_select_cell(&mut self, row_id: RowId, option_id: &str) { + let field = self + .editor + .get_fields(&self.view_id, None) + .iter() + .find(|field| { + let field_type = FieldType::from(field.field_type); + field_type == FieldType::SingleSelect + }) + .unwrap() + .clone(); + + let cell_changeset = SelectOptionCellChangeset::from_insert_option_id(option_id); + self.update_cell(&field.id, row_id, cell_changeset).await; + } +} + +pub struct TestRowBuilder { + row_id: RowId, + fields: Vec, + cell_build: CellBuilder, +} + +impl TestRowBuilder { + pub fn new(row_id: RowId, fields: Vec) -> Self { + let inner_builder = CellBuilder::with_cells(Default::default(), fields.clone()); + Self { + row_id, + fields, + cell_build: inner_builder, + } + } + + pub fn insert_text_cell(&mut self, data: &str) -> String { + let text_field = self.field_with_type(&FieldType::RichText); + self + .cell_build + .insert_text_cell(&text_field.id, data.to_string()); + + text_field.id.clone() + } + + pub fn insert_number_cell(&mut self, data: &str) -> String { + let number_field = self.field_with_type(&FieldType::Number); + self + .cell_build + .insert_text_cell(&number_field.id, data.to_string()); + number_field.id.clone() + } + + pub fn insert_date_cell(&mut self, data: &str) -> String { + let value = serde_json::to_string(&DateCellChangeset { + date: Some(data.to_string()), + time: None, + is_utc: true, + include_time: Some(false), + }) + .unwrap(); + let date_field = self.field_with_type(&FieldType::DateTime); + self.cell_build.insert_text_cell(&date_field.id, value); + date_field.id.clone() + } + + pub fn insert_checkbox_cell(&mut self, data: &str) -> String { + let checkbox_field = self.field_with_type(&FieldType::Checkbox); + self + .cell_build + .insert_text_cell(&checkbox_field.id, data.to_string()); + + checkbox_field.id.clone() + } + + pub fn insert_url_cell(&mut self, content: &str) -> String { + let url_field = self.field_with_type(&FieldType::URL); + self + .cell_build + .insert_url_cell(&url_field.id, content.to_string()); + url_field.id.clone() + } + + pub fn insert_single_select_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> SelectOption, + { + let single_select_field = self.field_with_type(&FieldType::SingleSelect); + let type_option = single_select_field + .get_type_option::(FieldType::SingleSelect) + .unwrap(); + let option = f(type_option.options); + self + .cell_build + .insert_select_option_cell(&single_select_field.id, vec![option.id]); + + single_select_field.id.clone() + } + + pub fn insert_multi_select_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> Vec, + { + let multi_select_field = self.field_with_type(&FieldType::MultiSelect); + let type_option = multi_select_field + .get_type_option::(FieldType::MultiSelect) + .unwrap(); + let options = f(type_option.options); + let ops_ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>(); + self + .cell_build + .insert_select_option_cell(&multi_select_field.id, ops_ids); + + multi_select_field.id.clone() + } + + pub fn insert_checklist_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> Vec, + { + let checklist_field = self.field_with_type(&FieldType::Checklist); + let type_option = checklist_field + .get_type_option::(FieldType::Checklist) + .unwrap(); + let options = f(type_option.options); + let ops_ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>(); + self + .cell_build + .insert_select_option_cell(&checklist_field.id, ops_ids); + + checklist_field.id.clone() + } + + pub fn field_with_type(&self, field_type: &FieldType) -> Field { + self + .fields + .iter() + .find(|field| { + let t_field_type = FieldType::from(field.field_type); + &t_field_type == field_type + }) + .unwrap() + .clone() + } + + pub fn build(self) -> CreateRowParams { + CreateRowParams { + id: self.row_id, + cells: self.cell_build.build(), + height: 60, + visibility: true, + prev_row_id: None, + timestamp: 0, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/mod.rs new file mode 100644 index 0000000000..5ac4da9f24 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/mod.rs @@ -0,0 +1,3 @@ +mod script; +mod test; +pub mod util; diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs new file mode 100644 index 0000000000..97a5bfd272 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs @@ -0,0 +1,153 @@ +use collab_database::fields::{Field, TypeOptionData}; + +use flowy_database2::entities::{CreateFieldParams, FieldChangesetParams, FieldType}; +use flowy_database2::services::cell::stringify_cell_data; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum FieldScript { + CreateField { + params: CreateFieldParams, + }, + UpdateField { + changeset: FieldChangesetParams, + }, + DeleteField { + field: Field, + }, + SwitchToField { + field_id: String, + new_field_type: FieldType, + }, + UpdateTypeOption { + field_id: String, + type_option: TypeOptionData, + }, + AssertFieldCount(usize), + AssertFieldTypeOptionEqual { + field_index: usize, + expected_type_option_data: TypeOptionData, + }, + AssertCellContent { + field_id: String, + row_index: usize, + from_field_type: FieldType, + expected_content: String, + }, +} + +pub struct DatabaseFieldTest { + inner: DatabaseEditorTest, +} + +impl DatabaseFieldTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { inner: editor_test } + } + + pub fn view_id(&self) -> String { + self.view_id.clone() + } + + pub fn field_count(&self) -> usize { + self.field_count + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: FieldScript) { + match script { + FieldScript::CreateField { params } => { + self.field_count += 1; + self + .editor + .create_field_with_type_option(&self.view_id, ¶ms.field_type, params.type_option_data) + .await; + let fields = self.editor.get_fields(&self.view_id, None); + assert_eq!(self.field_count, fields.len()); + }, + FieldScript::UpdateField { changeset: change } => { + self.editor.update_field(change).await.unwrap(); + }, + FieldScript::DeleteField { field } => { + if self.editor.get_field(&field.id).is_some() { + self.field_count -= 1; + } + + self.editor.delete_field(&field.id).await.unwrap(); + let fields = self.editor.get_fields(&self.view_id, None); + assert_eq!(self.field_count, fields.len()); + }, + FieldScript::SwitchToField { + field_id, + new_field_type, + } => { + // + self + .editor + .switch_to_field_type(&field_id, &new_field_type) + .await + .unwrap(); + }, + FieldScript::UpdateTypeOption { + field_id, + type_option, + } => { + // + let old_field = self.editor.get_field(&field_id).unwrap(); + self + .editor + .update_field_type_option(&self.view_id, &field_id, type_option, old_field) + .await + .unwrap(); + }, + FieldScript::AssertFieldCount(count) => { + assert_eq!(self.get_fields().len(), count); + }, + FieldScript::AssertFieldTypeOptionEqual { + field_index, + expected_type_option_data, + } => { + let fields = self.get_fields(); + let field = &fields[field_index]; + let type_option_data = field.get_any_type_option(field.field_type).unwrap(); + assert_eq!(type_option_data, expected_type_option_data); + }, + FieldScript::AssertCellContent { + field_id, + row_index, + from_field_type, + expected_content, + } => { + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + + let rows = self.editor.get_rows(&self.view_id()).await.unwrap(); + let row = rows.get(row_index).unwrap(); + + let cell = row.cells.get(&field_id).unwrap().clone(); + let content = stringify_cell_data(&cell, &from_field_type, &field_type, &field); + assert_eq!(content, expected_content); + }, + } + } +} + +impl std::ops::Deref for DatabaseFieldTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for DatabaseFieldTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs new file mode 100644 index 0000000000..c5df56e207 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -0,0 +1,306 @@ +use collab_database::database::gen_option_id; + +use flowy_database2::entities::{FieldChangesetParams, FieldType}; +use flowy_database2::services::field::{SelectOption, CHECK, UNCHECK}; + +use crate::database::field_test::script::DatabaseFieldTest; +use crate::database::field_test::script::FieldScript::*; +use crate::database::field_test::util::*; + +#[tokio::test] +async fn grid_create_field() { + let mut test = DatabaseFieldTest::new().await; + let (params, field) = create_text_field(&test.view_id()); + + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; + + let (params, field) = create_single_select_field(&test.view_id()); + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_create_duplicate_field() { + let mut test = DatabaseFieldTest::new().await; + let (params, _) = create_text_field(&test.view_id()); + let field_count = test.field_count(); + let expected_field_count = field_count + 1; + let scripts = vec![ + CreateField { + params: params.clone(), + }, + AssertFieldCount(expected_field_count), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_field_with_empty_change() { + let mut test = DatabaseFieldTest::new().await; + let (params, _) = create_single_select_field(&test.view_id()); + let create_field_index = test.field_count(); + let scripts = vec![CreateField { params }]; + test.run_scripts(scripts).await; + + let field = test.get_fields().pop().unwrap().clone(); + let changeset = FieldChangesetParams { + field_id: field.id.clone(), + view_id: test.view_id(), + ..Default::default() + }; + + let scripts = vec![ + UpdateField { changeset }, + AssertFieldTypeOptionEqual { + field_index: create_field_index, + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_delete_field() { + let mut test = DatabaseFieldTest::new().await; + let original_field_count = test.field_count(); + let (params, _) = create_text_field(&test.view_id()); + let scripts = vec![CreateField { params }]; + test.run_scripts(scripts).await; + + let field = test.get_fields().pop().unwrap(); + let scripts = vec![ + DeleteField { field }, + AssertFieldCount(original_field_count), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_switch_from_select_option_to_checkbox_test() { + let mut test = DatabaseFieldTest::new().await; + let field = test.get_first_field(FieldType::SingleSelect); + + // Update the type option data of single select option + let mut single_select_type_option = test.get_single_select_type_option(&field.id); + single_select_type_option.options.clear(); + // Add a new option with name CHECK + single_select_type_option.options.push(SelectOption { + id: gen_option_id(), + name: CHECK.to_string(), + color: Default::default(), + }); + // Add a new option with name UNCHECK + single_select_type_option.options.push(SelectOption { + id: gen_option_id(), + name: UNCHECK.to_string(), + color: Default::default(), + }); + + let scripts = vec![ + UpdateTypeOption { + field_id: field.id.clone(), + type_option: single_select_type_option.into(), + }, + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::Checkbox, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_switch_from_checkbox_to_select_option_test() { + let mut test = DatabaseFieldTest::new().await; + let checkbox_field = test.get_first_field(FieldType::Checkbox).clone(); + let scripts = vec![ + // switch to single-select field type + SwitchToField { + field_id: checkbox_field.id.clone(), + new_field_type: FieldType::SingleSelect, + }, + // Assert the cell content after switch the field type. The cell content will be changed if + // the FieldType::SingleSelect implement the cell data TypeOptionTransform. Check out the + // TypeOptionTransform trait for more information. + // + // Make sure which cell of the row you want to check. + AssertCellContent { + field_id: checkbox_field.id.clone(), + // the mock data of the checkbox with row_index one is "true" + row_index: 1, + // the from_field_type represents as the current field type + from_field_type: FieldType::Checkbox, + // The content of the checkbox should transform to the corresponding option name. + expected_content: CHECK.to_string(), + }, + ]; + test.run_scripts(scripts).await; + + let single_select_type_option = test.get_single_select_type_option(&checkbox_field.id); + assert_eq!(single_select_type_option.options.len(), 2); + assert!(single_select_type_option + .options + .iter() + .any(|option| option.name == UNCHECK)); + assert!(single_select_type_option + .options + .iter() + .any(|option| option.name == CHECK)); +} + +// Test when switching the current field from Multi-select to Text test +// The build-in test data is located in `make_test_grid` method(flowy-database/tests/grid_editor.rs). +// input: +// option1, option2 -> "option1.name, option2.name" +#[tokio::test] +async fn grid_switch_from_multi_select_to_text_test() { + let mut test = DatabaseFieldTest::new().await; + let field_rev = test.get_first_field(FieldType::MultiSelect).clone(); + + let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id); + + let script_switch_field = vec![SwitchToField { + field_id: field_rev.id.clone(), + new_field_type: FieldType::RichText, + }]; + + test.run_scripts(script_switch_field).await; + + let script_assert_field = vec![AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 0, + from_field_type: FieldType::MultiSelect, + expected_content: format!( + "{},{}", + multi_select_type_option.get(0).unwrap().name, + multi_select_type_option.get(1).unwrap().name + ), + }]; + + test.run_scripts(script_assert_field).await; +} + +// Test when switching the current field from Checkbox to Text test +// input: +// check -> "Yes" +// unchecked -> "" +#[tokio::test] +async fn grid_switch_from_checkbox_to_text_test() { + let mut test = DatabaseFieldTest::new().await; + let field_rev = test.get_first_field(FieldType::Checkbox); + + let scripts = vec![ + SwitchToField { + field_id: field_rev.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 1, + from_field_type: FieldType::Checkbox, + expected_content: "Yes".to_string(), + }, + AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 2, + from_field_type: FieldType::Checkbox, + expected_content: "No".to_string(), + }, + ]; + test.run_scripts(scripts).await; +} + +// Test when switching the current field from Checkbox to Text test +// input: +// "Yes" -> check +// "" -> unchecked +#[tokio::test] +async fn grid_switch_from_text_to_checkbox_test() { + let mut test = DatabaseFieldTest::new().await; + let field = test.get_first_field(FieldType::RichText).clone(); + + let scripts = vec![ + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::Checkbox, + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 0, + from_field_type: FieldType::RichText, + expected_content: "".to_string(), + }, + ]; + test.run_scripts(scripts).await; +} + +// Test when switching the current field from Date to Text test +// input: +// 1647251762 -> Mar 14,2022 (This string will be different base on current data setting) +#[tokio::test] +async fn grid_switch_from_date_to_text_test() { + let mut test = DatabaseFieldTest::new().await; + let field = test.get_first_field(FieldType::DateTime).clone(); + let scripts = vec![ + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 2, + from_field_type: FieldType::DateTime, + expected_content: "2022/03/14".to_string(), + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 3, + from_field_type: FieldType::DateTime, + expected_content: "2022/11/17".to_string(), + }, + ]; + test.run_scripts(scripts).await; +} + +// Test when switching the current field from Number to Text test +// input: +// $1 -> "$1"(This string will be different base on current data setting) +#[tokio::test] +async fn grid_switch_from_number_to_text_test() { + let mut test = DatabaseFieldTest::new().await; + let field = test.get_first_field(FieldType::Number).clone(); + + let scripts = vec![ + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 0, + from_field_type: FieldType::Number, + expected_content: "$1".to_string(), + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 4, + from_field_type: FieldType::Number, + expected_content: "".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs new file mode 100644 index 0000000000..e3d9880f0c --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs @@ -0,0 +1,55 @@ +use collab_database::fields::Field; +use flowy_database2::entities::{CreateFieldParams, FieldType}; +use flowy_database2::services::field::{ + type_option_to_pb, DateCellChangeset, FieldBuilder, RichTextTypeOption, SelectOption, + SingleSelectTypeOption, +}; + +pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) { + let field_type = FieldType::RichText; + let type_option = RichTextTypeOption::default(); + let text_field = FieldBuilder::new(field_type.clone(), type_option.clone()) + .name("Name") + .visibility(true) + .primary(true) + .build(); + + let type_option_data = type_option_to_pb(type_option.into(), &field_type).to_vec(); + let params = CreateFieldParams { + view_id: grid_id.to_owned(), + field_type, + type_option_data: Some(type_option_data), + }; + (params, text_field) +} + +pub fn create_single_select_field(grid_id: &str) -> (CreateFieldParams, Field) { + let field_type = FieldType::SingleSelect; + let mut type_option = SingleSelectTypeOption::default(); + type_option.options.push(SelectOption::new("Done")); + type_option.options.push(SelectOption::new("Progress")); + let single_select_field = FieldBuilder::new(field_type.clone(), type_option.clone()) + .name("Name") + .visibility(true) + .build(); + + let type_option_data = type_option_to_pb(type_option.into(), &field_type).to_vec(); + let params = CreateFieldParams { + view_id: grid_id.to_owned(), + field_type, + type_option_data: Some(type_option_data), + }; + (params, single_select_field) +} + +// The grid will contains all existing field types and there are three empty rows in this grid. + +pub fn make_date_cell_string(s: &str) -> String { + serde_json::to_string(&DateCellChangeset { + date: Some(s.to_string()), + time: None, + is_utc: true, + include_time: Some(false), + }) + .unwrap() +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs new file mode 100644 index 0000000000..bb8d193f54 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs @@ -0,0 +1,37 @@ +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; +use flowy_database2::entities::CheckboxFilterConditionPB; + +#[tokio::test] +async fn grid_filter_checkbox_is_check_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + // The initial number of unchecked is 3 + // The initial number of checked is 2 + let scripts = vec![CreateCheckboxFilter { + condition: CheckboxFilterConditionPB::IsChecked, + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - 3, + }), + }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_checkbox_is_uncheck_test() { + let mut test = DatabaseFilterTest::new().await; + let expected = 3; + let row_count = test.rows.len(); + let scripts = vec![ + CreateCheckboxFilter { + condition: CheckboxFilterConditionPB::IsUnChecked, + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs new file mode 100644 index 0000000000..7f3fac9e2e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -0,0 +1,39 @@ +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; +use flowy_database2::entities::ChecklistFilterConditionPB; + +#[tokio::test] +async fn grid_filter_checklist_is_incomplete_test() { + let mut test = DatabaseFilterTest::new().await; + let expected = 5; + let row_count = test.rows.len(); + let scripts = vec![ + CreateChecklistFilter { + condition: ChecklistFilterConditionPB::IsIncomplete, + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_checklist_is_complete_test() { + let mut test = DatabaseFilterTest::new().await; + let expected = 1; + let row_count = test.rows.len(); + let scripts = vec![ + CreateChecklistFilter { + condition: ChecklistFilterConditionPB::IsComplete, + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs new file mode 100644 index 0000000000..0c73464bb1 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs @@ -0,0 +1,108 @@ +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; +use flowy_database2::entities::DateFilterConditionPB; + +#[tokio::test] +async fn grid_filter_date_is_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 3; + let scripts = vec![ + CreateDateFilter { + condition: DateFilterConditionPB::DateIs, + start: None, + end: None, + timestamp: Some(1647251762), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_date_after_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 3; + let scripts = vec![ + CreateDateFilter { + condition: DateFilterConditionPB::DateAfter, + start: None, + end: None, + timestamp: Some(1647251762), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_date_on_or_after_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 3; + let scripts = vec![ + CreateDateFilter { + condition: DateFilterConditionPB::DateOnOrAfter, + start: None, + end: None, + timestamp: Some(1668359085), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_date_on_or_before_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 4; + let scripts = vec![ + CreateDateFilter { + condition: DateFilterConditionPB::DateOnOrBefore, + start: None, + end: None, + timestamp: Some(1668359085), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_date_within_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 5; + let scripts = vec![ + CreateDateFilter { + condition: DateFilterConditionPB::DateWithIn, + start: Some(1647251762), + end: Some(1668704685), + timestamp: None, + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs new file mode 100644 index 0000000000..160bf3427f --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs @@ -0,0 +1,7 @@ +mod checkbox_filter_test; +mod checklist_filter_test; +mod date_filter_test; +mod number_filter_test; +mod script; +mod select_option_filter_test; +mod text_filter_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs new file mode 100644 index 0000000000..fb31799039 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs @@ -0,0 +1,118 @@ +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; +use flowy_database2::entities::NumberFilterConditionPB; + +#[tokio::test] +async fn grid_filter_number_is_equal_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 1; + let scripts = vec![ + CreateNumberFilter { + condition: NumberFilterConditionPB::Equal, + content: "1".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_number_is_less_than_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 2; + let scripts = vec![ + CreateNumberFilter { + condition: NumberFilterConditionPB::LessThan, + content: "3".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +#[should_panic] +async fn grid_filter_number_is_less_than_test2() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 2; + let scripts = vec![ + CreateNumberFilter { + condition: NumberFilterConditionPB::LessThan, + content: "$3".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_number_is_less_than_or_equal_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 3; + let scripts = vec![ + CreateNumberFilter { + condition: NumberFilterConditionPB::LessThanOrEqualTo, + content: "3".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_number_is_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 1; + let scripts = vec![ + CreateNumberFilter { + condition: NumberFilterConditionPB::NumberIsEmpty, + content: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_number_is_not_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.rows.len(); + let expected = 5; + let scripts = vec![ + CreateNumberFilter { + condition: NumberFilterConditionPB::NumberIsNotEmpty, + content: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs new file mode 100644 index 0000000000..e94b0bac36 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -0,0 +1,312 @@ +#![cfg_attr(rustfmt, rustfmt::skip)] +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_imports)] + +use std::time::Duration; + +use bytes::Bytes; +use collab_database::rows::{Row, RowId}; +use futures::TryFutureExt; +use tokio::sync::broadcast::Receiver; + +use flowy_database2::entities::{AlterFilterParams, AlterFilterPayloadPB, CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterParams, FieldType, FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, TextFilterConditionPB, TextFilterPB}; +use flowy_database2::services::database_view::DatabaseViewChanged; +use flowy_database2::services::filter::FilterType; + +use crate::database::database_editor::DatabaseEditorTest; + +pub struct FilterRowChanged { + pub(crate) showing_num_of_rows: usize, + pub(crate) hiding_num_of_rows: usize, +} + +pub enum FilterScript { + UpdateTextCell { + row_id: RowId, + text: String, + changed: Option, + }, + UpdateSingleSelectCell { + row_id: RowId, + option_id: String, + changed: Option, + }, + InsertFilter { + payload: AlterFilterPayloadPB, + }, + CreateTextFilter { + condition: TextFilterConditionPB, + content: String, + changed: Option, + }, + UpdateTextFilter { + filter: FilterPB, + condition: TextFilterConditionPB, + content: String, + changed: Option, + }, + CreateNumberFilter { + condition: NumberFilterConditionPB, + content: String, + changed: Option, + }, + CreateCheckboxFilter { + condition: CheckboxFilterConditionPB, + changed: Option, + }, + CreateDateFilter{ + condition: DateFilterConditionPB, + start: Option, + end: Option, + timestamp: Option, + changed: Option, + }, + CreateMultiSelectFilter { + condition: SelectOptionConditionPB, + option_ids: Vec, + }, + CreateSingleSelectFilter { + condition: SelectOptionConditionPB, + option_ids: Vec, + changed: Option, + }, + CreateChecklistFilter { + condition: ChecklistFilterConditionPB, + changed: Option, + }, + AssertFilterCount { + count: i32, + }, + DeleteFilter { + filter_id: String, + filter_type: FilterType, + changed: Option, + }, + AssertFilterContent { + filter_id: String, + condition: i64, + content: String + }, + AssertNumberOfVisibleRows { + expected: usize, + }, + #[allow(dead_code)] + AssertGridSetting { + expected_setting: DatabaseViewSettingPB, + }, + Wait { millisecond: u64 } +} + +pub struct DatabaseFilterTest { + inner: DatabaseEditorTest, + recv: Option>, +} + +impl DatabaseFilterTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { + inner: editor_test, + recv: None, + } + } + + pub fn view_id(&self) -> String { + self.view_id.clone() + } + + pub async fn get_all_filters(&self) -> Vec { + self.editor.get_all_filters(&self.view_id).await.items + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: FilterScript) { + match script { + FilterScript::UpdateTextCell { row_id, text, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + self.update_text_cell(row_id, &text).await; + } + FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + self.update_single_select_cell(row_id, &option_id).await; + } + FilterScript::InsertFilter { payload } => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.insert_filter(payload).await; + } + FilterScript::CreateTextFilter { condition, content, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let field = self.get_first_field(FieldType::RichText); + let text_filter= TextFilterPB { + condition, + content + }; + let payload = + AlterFilterPayloadPB::new( + & self.view_id(), + &field, text_filter); + self.insert_filter(payload).await; + } + FilterScript::UpdateTextFilter { filter, condition, content, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let params = AlterFilterParams { + view_id: self.view_id(), + field_id: filter.field_id, + filter_id: Some(filter.id), + field_type: filter.field_type.into(), + condition: condition as i64, + content + }; + self.editor.create_or_update_filter(params).await.unwrap(); + } + FilterScript::CreateNumberFilter {condition, content, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let field = self.get_first_field(FieldType::Number); + let number_filter = NumberFilterPB { + condition, + content + }; + let payload = + AlterFilterPayloadPB::new( + &self.view_id(), + &field, number_filter); + self.insert_filter(payload).await; + } + FilterScript::CreateCheckboxFilter {condition, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let field = self.get_first_field(FieldType::Checkbox); + let checkbox_filter = CheckboxFilterPB { + condition + }; + let payload = + AlterFilterPayloadPB::new(& self.view_id(), &field, checkbox_filter); + self.insert_filter(payload).await; + } + FilterScript::CreateDateFilter { condition, start, end, timestamp, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let field = self.get_first_field(FieldType::DateTime); + let date_filter = DateFilterPB { + condition, + start, + end, + timestamp + }; + + let payload = + AlterFilterPayloadPB::new(&self.view_id(), &field, date_filter); + self.insert_filter(payload).await; + } + FilterScript::CreateMultiSelectFilter { condition, option_ids} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + let field = self.get_first_field(FieldType::MultiSelect); + let filter = SelectOptionFilterPB { condition, option_ids }; + let payload = + AlterFilterPayloadPB::new(&self.view_id(), &field, filter); + self.insert_filter(payload).await; + } + FilterScript::CreateSingleSelectFilter { condition, option_ids, changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let field = self.get_first_field(FieldType::SingleSelect); + let filter = SelectOptionFilterPB { condition, option_ids }; + let payload = + AlterFilterPayloadPB::new(& self.view_id(), &field, filter); + self.insert_filter(payload).await; + } + FilterScript::CreateChecklistFilter { condition,changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let field = self.get_first_field(FieldType::Checklist); + // let type_option = self.get_checklist_type_option(&field_rev.id); + let filter = ChecklistFilterPB { condition }; + let payload = + AlterFilterPayloadPB::new(& self.view_id(), &field, filter); + self.insert_filter(payload).await; + } + FilterScript::AssertFilterCount { count } => { + let filters = self.editor.get_all_filters(&self.view_id).await.items; + assert_eq!(count as usize, filters.len()); + } + FilterScript::AssertFilterContent { filter_id, condition, content} => { + let filter = self.editor.get_filter(&self.view_id, &filter_id).await.unwrap(); + assert_eq!(&filter.content, &content); + assert_eq!(filter.condition, condition); + + } + FilterScript::DeleteFilter { filter_id, filter_type ,changed} => { + self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); + self.assert_future_changed(changed).await; + let params = DeleteFilterParams { filter_id, view_id: self.view_id(),filter_type }; + let _ = self.editor.delete_filter(params).await.unwrap(); + } + FilterScript::AssertGridSetting { expected_setting } => { + let setting = self.editor.get_database_view_setting(&self.view_id).await.unwrap(); + assert_eq!(expected_setting, setting); + } + FilterScript::AssertNumberOfVisibleRows { expected } => { + let grid = self.editor.get_database_data(&self.view_id).await; + assert_eq!(grid.rows.len(), expected); + } + FilterScript::Wait { millisecond } => { + tokio::time::sleep(Duration::from_millis(millisecond)).await; + } + } + } + + async fn assert_future_changed(&mut self, change: Option) { + if change.is_none() {return;} + let change = change.unwrap(); + let mut receiver = self.recv.take().unwrap(); + tokio::spawn(async move { + match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await { + Ok(changed) => { + match changed.unwrap() { DatabaseViewChanged::FilterNotification(notification) => { + assert_eq!(notification.visible_rows.len(), change.showing_num_of_rows, "visible rows not match"); + assert_eq!(notification.invisible_rows.len(), change.hiding_num_of_rows, "invisible rows not match"); + } + _ => {} + } + }, + Err(e) => { + panic!("Process filter task timeout: {:?}", e); + } + } + }); + + + } + + async fn insert_filter(&self, payload: AlterFilterPayloadPB) { + let params: AlterFilterParams = payload.try_into().unwrap(); + let _ = self.editor.create_or_update_filter(params).await.unwrap(); + } + +} + + +impl std::ops::Deref for DatabaseFilterTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for DatabaseFilterTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs new file mode 100644 index 0000000000..6a920bad71 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs @@ -0,0 +1,137 @@ +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; +use flowy_database2::entities::{FieldType, SelectOptionConditionPB}; + +#[tokio::test] +async fn grid_filter_multi_select_is_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![ + CreateMultiSelectFilter { + condition: SelectOptionConditionPB::OptionIsEmpty, + option_ids: vec![], + }, + AssertNumberOfVisibleRows { expected: 3 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_multi_select_is_not_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![ + CreateMultiSelectFilter { + condition: SelectOptionConditionPB::OptionIsNotEmpty, + option_ids: vec![], + }, + AssertNumberOfVisibleRows { expected: 3 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_multi_select_is_test() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateMultiSelectFilter { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![options.remove(0).id, options.remove(0).id], + }, + AssertNumberOfVisibleRows { expected: 3 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_multi_select_is_test2() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateMultiSelectFilter { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![options.remove(1).id], + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_single_select_is_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let expected = 2; + let row_count = test.rows.len(); + let scripts = vec![ + CreateSingleSelectFilter { + condition: SelectOptionConditionPB::OptionIsEmpty, + option_ids: vec![], + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_single_select_is_test() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::SingleSelect); + let mut options = test.get_single_select_type_option(&field.id).options; + let expected = 2; + let row_count = test.rows.len(); + let scripts = vec![ + CreateSingleSelectFilter { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![options.remove(0).id], + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_single_select_is_test2() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::SingleSelect); + let rows = test.get_rows().await; + let mut options = test.get_single_select_type_option(&field.id).options; + let option = options.remove(0); + let row_count = test.rows.len(); + + let scripts = vec![ + CreateSingleSelectFilter { + condition: SelectOptionConditionPB::OptionIs, + option_ids: vec![option.id.clone()], + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - 2, + }), + }, + AssertNumberOfVisibleRows { expected: 2 }, + UpdateSingleSelectCell { + row_id: rows[1].id, + option_id: option.id.clone(), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 3 }, + UpdateSingleSelectCell { + row_id: rows[1].id, + option_id: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 1, + }), + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs new file mode 100644 index 0000000000..eb16d3a5a5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs @@ -0,0 +1,244 @@ +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::*; +use flowy_database2::entities::{ + AlterFilterPayloadPB, FieldType, TextFilterConditionPB, TextFilterPB, +}; +use flowy_database2::services::filter::FilterType; + +#[tokio::test] +async fn grid_filter_text_is_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterConditionPB::TextIsEmpty, + content: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 5, + }), + }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_text_is_not_empty_test() { + let mut test = DatabaseFilterTest::new().await; + // Only one row's text of the initial rows is "" + let scripts = vec![ + CreateTextFilter { + condition: TextFilterConditionPB::TextIsNotEmpty, + content: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 1, + }), + }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; + + let filter = test.database_filters().await.pop().unwrap(); + test + .run_scripts(vec![ + DeleteFilter { + filter_id: filter.id.clone(), + filter_type: FilterType::from(&filter), + changed: Some(FilterRowChanged { + showing_num_of_rows: 1, + hiding_num_of_rows: 0, + }), + }, + AssertFilterCount { count: 0 }, + ]) + .await; +} + +#[tokio::test] +async fn grid_filter_is_text_test() { + let mut test = DatabaseFilterTest::new().await; + // Only one row's text of the initial rows is "A" + let scripts = vec![CreateTextFilter { + condition: TextFilterConditionPB::Is, + content: "A".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 5, + }), + }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_contain_text_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![CreateTextFilter { + condition: TextFilterConditionPB::Contains, + content: "A".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 2, + }), + }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_contain_text_test2() { + let mut test = DatabaseFilterTest::new().await; + let rows = test.rows.clone(); + + let scripts = vec![ + CreateTextFilter { + condition: TextFilterConditionPB::Contains, + content: "A".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 2, + }), + }, + UpdateTextCell { + row_id: rows[1].id, + text: "ABC".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 1, + hiding_num_of_rows: 0, + }), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_does_not_contain_text_test() { + let mut test = DatabaseFilterTest::new().await; + // None of the initial rows contains the text "AB" + let scripts = vec![CreateTextFilter { + condition: TextFilterConditionPB::DoesNotContain, + content: "AB".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 0, + }), + }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_start_with_text_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![CreateTextFilter { + condition: TextFilterConditionPB::StartsWith, + content: "A".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 3, + }), + }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_ends_with_text_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterConditionPB::EndsWith, + content: "A".to_string(), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_text_filter_test() { + let mut test = DatabaseFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterConditionPB::EndsWith, + content: "A".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 4, + }), + }, + AssertNumberOfVisibleRows { expected: 2 }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; + + // Update the filter + let filter = test.get_all_filters().await.pop().unwrap(); + let scripts = vec![ + UpdateTextFilter { + filter, + condition: TextFilterConditionPB::Is, + content: "A".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 1, + }), + }, + AssertNumberOfVisibleRows { expected: 1 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_delete_test() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::RichText).clone(); + let text_filter = TextFilterPB { + condition: TextFilterConditionPB::TextIsEmpty, + content: "".to_string(), + }; + let payload = AlterFilterPayloadPB::new(&test.view_id(), &field, text_filter); + let scripts = vec![ + InsertFilter { payload }, + AssertFilterCount { count: 1 }, + AssertNumberOfVisibleRows { expected: 1 }, + ]; + test.run_scripts(scripts).await; + + let filter = test.database_filters().await.pop().unwrap(); + test + .run_scripts(vec![ + DeleteFilter { + filter_id: filter.id.clone(), + filter_type: FilterType::from(&filter), + changed: None, + }, + AssertFilterCount { count: 0 }, + AssertNumberOfVisibleRows { expected: 6 }, + ]) + .await; +} + +#[tokio::test] +async fn grid_filter_update_empty_text_cell_test() { + let mut test = DatabaseFilterTest::new().await; + let rows = test.rows.clone(); + let scripts = vec![ + CreateTextFilter { + condition: TextFilterConditionPB::TextIsEmpty, + content: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 5, + }), + }, + AssertFilterCount { count: 1 }, + UpdateTextCell { + row_id: rows[0].id, + text: "".to_string(), + changed: Some(FilterRowChanged { + showing_num_of_rows: 1, + hiding_num_of_rows: 0, + }), + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs new file mode 100644 index 0000000000..67671ae7f5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs @@ -0,0 +1,3 @@ +mod script; +mod test; +mod url_group_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs new file mode 100644 index 0000000000..5e765f5702 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -0,0 +1,327 @@ +use collab_database::fields::Field; +use collab_database::rows::RowId; +use flowy_database2::entities::{CreateRowParams, FieldType, GroupPB, RowPB}; +use flowy_database2::services::cell::{ + delete_select_option_cell, insert_select_option_cell, insert_url_cell, +}; +use flowy_database2::services::field::{ + edit_single_select_type_option, SelectOption, SelectTypeOptionSharedAction, + SingleSelectTypeOption, +}; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum GroupScript { + AssertGroupRowCount { + group_index: usize, + row_count: usize, + }, + AssertGroupCount(usize), + AssertGroup { + group_index: usize, + expected_group: GroupPB, + }, + AssertRow { + group_index: usize, + row_index: usize, + row: RowPB, + }, + MoveRow { + from_group_index: usize, + from_row_index: usize, + to_group_index: usize, + to_row_index: usize, + }, + CreateRow { + group_index: usize, + }, + DeleteRow { + group_index: usize, + row_index: usize, + }, + UpdateGroupedCell { + from_group_index: usize, + row_index: usize, + to_group_index: usize, + }, + UpdateGroupedCellWithData { + from_group_index: usize, + row_index: usize, + cell_data: String, + }, + MoveGroup { + from_group_index: usize, + to_group_index: usize, + }, + UpdateSingleSelectSelectOption { + inserted_options: Vec, + }, + GroupByField { + field_id: String, + }, +} + +pub struct DatabaseGroupTest { + inner: DatabaseEditorTest, +} + +impl DatabaseGroupTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_board().await; + Self { inner: editor_test } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: GroupScript) { + match script { + GroupScript::AssertGroupRowCount { + group_index, + row_count, + } => { + assert_eq!(row_count, self.group_at_index(group_index).await.rows.len()); + }, + GroupScript::AssertGroupCount(count) => { + let groups = self.editor.load_groups(&self.view_id).await.unwrap(); + assert_eq!(count, groups.len()); + }, + GroupScript::MoveRow { + from_group_index, + from_row_index, + to_group_index, + to_row_index, + } => { + let groups: Vec = self.editor.load_groups(&self.view_id).await.unwrap().items; + let from_row = groups + .get(from_group_index) + .unwrap() + .rows + .get(from_row_index) + .unwrap(); + let to_group = groups.get(to_group_index).unwrap(); + let to_row = to_group.rows.get(to_row_index).unwrap(); + + self + .editor + .move_group_row( + &self.view_id, + &to_group.group_id, + RowId::from(from_row.id), + Some(RowId::from(to_row.id)), + ) + .await + .unwrap(); + }, + GroupScript::AssertRow { + group_index, + row_index, + row, + } => { + // + let group = self.group_at_index(group_index).await; + let compare_row = group.rows.get(row_index).unwrap().clone(); + assert_eq!(row.id, compare_row.id); + }, + GroupScript::CreateRow { group_index } => { + let group = self.group_at_index(group_index).await; + let params = CreateRowParams { + view_id: self.view_id.clone(), + start_row_id: None, + group_id: Some(group.group_id.clone()), + cell_data_by_field_id: None, + }; + let _ = self.editor.create_row(params).await.unwrap(); + }, + GroupScript::DeleteRow { + group_index, + row_index, + } => { + let row = self.row_at_index(group_index, row_index).await; + self.editor.delete_row(RowId::from(row.id)).await; + }, + GroupScript::UpdateGroupedCell { + from_group_index, + row_index, + to_group_index, + } => { + let from_group = self.group_at_index(from_group_index).await; + let to_group = self.group_at_index(to_group_index).await; + let field_id = from_group.field_id; + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + + let cell = if to_group.is_default { + match field_type { + FieldType::SingleSelect => { + delete_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + FieldType::MultiSelect => { + delete_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + _ => { + panic!("Unsupported group field type"); + }, + } + } else { + match field_type { + FieldType::SingleSelect => { + insert_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + FieldType::MultiSelect => { + insert_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + FieldType::URL => insert_url_cell(to_group.group_id.clone(), &field), + _ => { + panic!("Unsupported group field type"); + }, + } + }; + + let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id); + self + .editor + .update_cell(&self.view_id, row_id, &field_id, cell) + .await; + }, + GroupScript::UpdateGroupedCellWithData { + from_group_index, + row_index, + cell_data, + } => { + let from_group = self.group_at_index(from_group_index).await; + let field_id = from_group.field_id; + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + let cell = match field_type { + FieldType::URL => insert_url_cell(cell_data, &field), + _ => { + panic!("Unsupported group field type"); + }, + }; + + let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id); + self + .editor + .update_cell(&self.view_id, row_id, &field_id, cell) + .await; + }, + GroupScript::MoveGroup { + from_group_index, + to_group_index, + } => { + let from_group = self.group_at_index(from_group_index).await; + let to_group = self.group_at_index(to_group_index).await; + self + .editor + .move_group(&self.view_id, &from_group.group_id, &to_group.group_id) + .await + .unwrap(); + // + }, + GroupScript::AssertGroup { + group_index, + expected_group: group_pb, + } => { + let group = self.group_at_index(group_index).await; + assert_eq!(group.group_id, group_pb.group_id); + assert_eq!(group.desc, group_pb.desc); + }, + GroupScript::UpdateSingleSelectSelectOption { inserted_options } => { + self + .edit_single_select_type_option(|type_option| { + for inserted_option in inserted_options { + type_option.insert_option(inserted_option); + } + }) + .await; + }, + GroupScript::GroupByField { field_id } => { + self + .editor + .group_by_field(&self.view_id, &field_id) + .await + .unwrap(); + }, + } + } + + pub async fn group_at_index(&self, index: usize) -> GroupPB { + let groups = self.editor.load_groups(&self.view_id).await.unwrap().items; + groups.get(index).unwrap().clone() + } + + pub async fn row_at_index(&self, group_index: usize, row_index: usize) -> RowPB { + let groups = self.group_at_index(group_index).await; + groups.rows.get(row_index).unwrap().clone() + } + + #[allow(dead_code)] + pub async fn get_multi_select_field(&self) -> Field { + self + .inner + .get_fields() + .into_iter() + .find(|field_rev| { + let field_type = FieldType::from(field_rev.field_type); + field_type.is_multi_select() + }) + .unwrap() + } + + pub async fn get_single_select_field(&self) -> Field { + self + .inner + .get_fields() + .into_iter() + .find(|field| { + let field_type = FieldType::from(field.field_type); + field_type.is_single_select() + }) + .unwrap() + } + + pub async fn edit_single_select_type_option( + &self, + action: impl FnOnce(&mut SingleSelectTypeOption), + ) { + let single_select = self.get_single_select_field().await; + edit_single_select_type_option( + &self.view_id, + &single_select.id, + self.editor.clone(), + action, + ) + .await + .unwrap(); + } + + pub async fn get_url_field(&self) -> Field { + self + .inner + .get_fields() + .into_iter() + .find(|field| { + let field_type = FieldType::from(field.field_type); + field_type.is_url() + }) + .unwrap() + } +} + +impl std::ops::Deref for DatabaseGroupTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for DatabaseGroupTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs new file mode 100644 index 0000000000..f92d8cec1e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs @@ -0,0 +1,488 @@ +use flowy_database2::services::field::SelectOption; + +use crate::database::group_test::script::DatabaseGroupTest; +use crate::database::group_test::script::GroupScript::*; + +#[tokio::test] +async fn group_init_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + AssertGroupCount(4), + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_row_test() { + let mut test = DatabaseGroupTest::new().await; + let group = test.group_at_index(1).await; + let scripts = vec![ + // Move the row at 0 in group0 to group1 at 1 + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 1, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertRow { + group_index: 1, + row_index: 1, + row: group.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_row_to_other_group_test() { + let mut test = DatabaseGroupTest::new().await; + let group = test.group_at_index(1).await; + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_two_row_to_other_group_test() { + let mut test = DatabaseGroupTest::new().await; + let group_1 = test.group_at_index(1).await; + let scripts = vec![ + // Move row at index 0 from group 1 to group 2 at index 1 + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group_1.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; + + let group_1 = test.group_at_index(1).await; + // Move row at index 0 from group 1 to group 2 at index 1 + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 0, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 4, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group_1.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_row_to_other_group_and_reorder_from_up_to_down_test() { + let mut test = DatabaseGroupTest::new().await; + let group_1 = test.group_at_index(1).await; + let group_2 = test.group_at_index(2).await; + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group_1.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; + + let scripts = vec![ + MoveRow { + from_group_index: 2, + from_row_index: 0, + to_group_index: 2, + to_row_index: 2, + }, + AssertRow { + group_index: 2, + row_index: 2, + row: group_2.rows.get(0).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_row_to_other_group_and_reorder_from_bottom_to_up_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }]; + test.run_scripts(scripts).await; + + let group = test.group_at_index(2).await; + let scripts = vec![ + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + MoveRow { + from_group_index: 2, + from_row_index: 2, + to_group_index: 2, + to_row_index: 0, + }, + AssertRow { + group_index: 2, + row_index: 0, + row: group.rows.get(2).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; +} +#[tokio::test] +async fn group_create_row_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + CreateRow { group_index: 1 }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + CreateRow { group_index: 2 }, + CreateRow { group_index: 2 }, + AssertGroupRowCount { + group_index: 2, + row_count: 4, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_delete_row_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + DeleteRow { + group_index: 1, + row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_delete_all_row_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + DeleteRow { + group_index: 1, + row_index: 0, + }, + DeleteRow { + group_index: 1, + row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_update_row_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + // Update the row at 0 in group0 by setting the row's group field data + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 2, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_reorder_group_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + // Update the row at 0 in group0 by setting the row's group field data + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 2, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_to_default_group_test() { + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_from_default_group_test() { + let mut test = DatabaseGroupTest::new().await; + // Move one row from group 1 to group 0 + let scripts = vec![ + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; + + // Move one row from group 0 to group 1 + let scripts = vec![ + UpdateGroupedCell { + from_group_index: 0, + row_index: 0, + to_group_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_group_test() { + let mut test = DatabaseGroupTest::new().await; + let group_0 = test.group_at_index(0).await; + let group_1 = test.group_at_index(1).await; + let scripts = vec![ + MoveGroup { + from_group_index: 0, + to_group_index: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + AssertGroup { + group_index: 0, + expected_group: group_1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 0, + }, + AssertGroup { + group_index: 1, + expected_group: group_0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_group_row_after_move_group_test() { + let mut test = DatabaseGroupTest::new().await; + let group_1 = test.group_at_index(1).await; + let group_2 = test.group_at_index(2).await; + let scripts = vec![ + MoveGroup { + from_group_index: 1, + to_group_index: 2, + }, + AssertGroup { + group_index: 1, + expected_group: group_2, + }, + AssertGroup { + group_index: 2, + expected_group: group_1, + }, + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_group_to_default_group_pos_test() { + let mut test = DatabaseGroupTest::new().await; + let group_0 = test.group_at_index(0).await; + let group_3 = test.group_at_index(3).await; + let scripts = vec![ + MoveGroup { + from_group_index: 3, + to_group_index: 0, + }, + AssertGroup { + group_index: 0, + expected_group: group_3, + }, + AssertGroup { + group_index: 1, + expected_group: group_0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_insert_single_select_option_test() { + let mut test = DatabaseGroupTest::new().await; + let new_option_name = "New option"; + let scripts = vec![ + AssertGroupCount(4), + UpdateSingleSelectSelectOption { + inserted_options: vec![SelectOption::new(new_option_name)], + }, + AssertGroupCount(5), + ]; + test.run_scripts(scripts).await; + let new_group = test.group_at_index(1).await; + assert_eq!(new_group.desc, new_option_name); +} + +#[tokio::test] +async fn group_group_by_other_field() { + let mut test = DatabaseGroupTest::new().await; + let multi_select_field = test.get_multi_select_field().await; + let scripts = vec![ + GroupByField { + field_id: multi_select_field.id.clone(), + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + AssertGroupCount(4), + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs new file mode 100644 index 0000000000..83a38b07d3 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs @@ -0,0 +1,148 @@ +use crate::database::group_test::script::DatabaseGroupTest; +use crate::database::group_test::script::GroupScript::*; + +#[tokio::test] +async fn group_group_by_url() { + let mut test = DatabaseGroupTest::new().await; + let url_field = test.get_url_field().await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + AssertGroupCount(3), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_alter_url_to_another_group_url_test() { + let mut test = DatabaseGroupTest::new().await; + let url_field = test.get_url_field().await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + // When moving the last row from 2nd group to 1nd group, the 2nd group will be removed + UpdateGroupedCell { + from_group_index: 2, + row_index: 0, + to_group_index: 1, + }, + AssertGroupCount(2), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_alter_url_to_new_url_test() { + let mut test = DatabaseGroupTest::new().await; + let url_field = test.get_url_field().await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // When moving the last row from 2nd group to 1nd group, the 2nd group will be removed + UpdateGroupedCellWithData { + from_group_index: 0, + row_index: 0, + cell_data: "https://github.com/AppFlowy-IO".to_string(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + AssertGroupCount(4), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn group_move_url_group_row_test() { + let mut test = DatabaseGroupTest::new().await; + let url_field = test.get_url_field().await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + AssertGroupCount(3), + MoveRow { + from_group_index: 0, + from_row_index: 0, + to_group_index: 1, + to_row_index: 0, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/mod.rs new file mode 100644 index 0000000000..63d424afaf --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/mod.rs @@ -0,0 +1,2 @@ +mod script; +mod test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs new file mode 100644 index 0000000000..356c1837e0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs @@ -0,0 +1,86 @@ +use collab_database::fields::Field; +use collab_database::views::DatabaseLayout; + +use flowy_database2::entities::FieldType; +use flowy_database2::services::setting::CalendarLayoutSetting; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum LayoutScript { + AssertCalendarLayoutSetting { expected: CalendarLayoutSetting }, + GetCalendarEvents, +} + +pub struct DatabaseLayoutTest { + database_test: DatabaseEditorTest, +} + +impl DatabaseLayoutTest { + pub async fn new_calendar() -> Self { + let database_test = DatabaseEditorTest::new_calendar().await; + Self { database_test } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn get_first_date_field(&self) -> Field { + self.database_test.get_first_field(FieldType::DateTime) + } + + pub async fn run_script(&mut self, script: LayoutScript) { + match script { + LayoutScript::AssertCalendarLayoutSetting { expected } => { + let view_id = self.database_test.view_id.clone(); + let layout_ty = DatabaseLayout::Calendar; + + let calendar_setting = self + .database_test + .editor + .get_layout_setting(&view_id, layout_ty) + .await + .unwrap() + .calendar + .unwrap(); + + assert_eq!(calendar_setting.layout_ty, expected.layout_ty); + assert_eq!( + calendar_setting.first_day_of_week, + expected.first_day_of_week + ); + assert_eq!(calendar_setting.show_weekends, expected.show_weekends); + }, + LayoutScript::GetCalendarEvents => { + let events = self + .database_test + .editor + .get_all_calendar_events(&self.database_test.view_id) + .await; + assert_eq!(events.len(), 5); + + for (index, event) in events.into_iter().enumerate() { + if index == 0 { + assert_eq!(event.title, "A"); + assert_eq!(event.timestamp, 1678090778); + } + + if index == 1 { + assert_eq!(event.title, "B"); + assert_eq!(event.timestamp, 1677917978); + } + if index == 2 { + assert_eq!(event.title, "C"); + assert_eq!(event.timestamp, 1679213978); + } + if index == 4 { + assert_eq!(event.title, "E"); + assert_eq!(event.timestamp, 1678695578); + } + } + }, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs new file mode 100644 index 0000000000..10824c5bdc --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs @@ -0,0 +1,22 @@ +use flowy_database2::services::setting::CalendarLayoutSetting; + +use crate::database::layout_test::script::DatabaseLayoutTest; +use crate::database::layout_test::script::LayoutScript::*; + +#[tokio::test] +async fn calendar_initial_layout_setting_test() { + let mut test = DatabaseLayoutTest::new_calendar().await; + let date_field = test.get_first_date_field().await; + let default_calendar_setting = CalendarLayoutSetting::new(date_field.id.clone()); + let scripts = vec![AssertCalendarLayoutSetting { + expected: default_calendar_setting, + }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn calendar_get_events_test() { + let mut test = DatabaseLayoutTest::new_calendar().await; + let scripts = vec![GetCalendarEvents]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs new file mode 100644 index 0000000000..4c31b9345a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -0,0 +1,225 @@ +// #![allow(clippy::all)] +// #![allow(dead_code)] +// #![allow(unused_imports)] + +use crate::database::database_editor::TestRowBuilder; +use crate::database::mock_data::{ + COMPLETED, FACEBOOK, FIRST_THING, GOOGLE, PAUSED, PLANNED, SECOND_THING, THIRD_THING, TWITTER, +}; +use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData}; +use collab_database::views::{DatabaseLayout, DatabaseView}; +use flowy_database2::entities::FieldType; +use flowy_database2::services::field::{ + ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, + SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, +}; +use strum::IntoEnumIterator; + +// Kanban board unit test mock data +pub fn make_test_board() -> DatabaseData { + let mut fields = vec![]; + let mut rows = vec![]; + // Iterate through the FieldType to create the corresponding Field. + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => { + let text_field = FieldBuilder::from_field_type(field_type.clone()) + .name("Name") + .visibility(true) + .primary(true) + .build(); + fields.push(text_field); + }, + FieldType::Number => { + // Number + let number_field = FieldBuilder::from_field_type(field_type.clone()) + .name("Price") + .visibility(true) + .build(); + fields.push(number_field); + }, + FieldType::DateTime => { + // Date + let date_type_option = DateTypeOption { + date_format: DateFormat::US, + time_format: TimeFormat::TwentyFourHour, + include_time: false, + }; + let date_field = FieldBuilder::new(field_type.clone(), date_type_option) + .name("Time") + .visibility(true) + .build(); + fields.push(date_field); + }, + FieldType::SingleSelect => { + // Single Select + let option1 = SelectOption::with_color(COMPLETED, SelectOptionColor::Purple); + let option2 = SelectOption::with_color(PLANNED, SelectOptionColor::Orange); + let option3 = SelectOption::with_color(PAUSED, SelectOptionColor::Yellow); + let mut single_select_type_option = SingleSelectTypeOption::default(); + single_select_type_option + .options + .extend(vec![option1, option2, option3]); + let single_select_field = FieldBuilder::new(field_type.clone(), single_select_type_option) + .name("Status") + .visibility(true) + .build(); + fields.push(single_select_field); + }, + FieldType::MultiSelect => { + // MultiSelect + let option1 = SelectOption::with_color(GOOGLE, SelectOptionColor::Purple); + let option2 = SelectOption::with_color(FACEBOOK, SelectOptionColor::Orange); + let option3 = SelectOption::with_color(TWITTER, SelectOptionColor::Yellow); + let mut type_option = MultiSelectTypeOption::default(); + type_option.options.extend(vec![option1, option2, option3]); + let multi_select_field = FieldBuilder::new(field_type.clone(), type_option) + .name("Platform") + .visibility(true) + .build(); + fields.push(multi_select_field); + }, + FieldType::Checkbox => { + // Checkbox + let checkbox_field = FieldBuilder::from_field_type(field_type.clone()) + .name("is urgent") + .visibility(true) + .build(); + fields.push(checkbox_field); + }, + FieldType::URL => { + // URL + let url = FieldBuilder::from_field_type(field_type.clone()) + .name("link") + .visibility(true) + .build(); + fields.push(url); + }, + FieldType::Checklist => { + let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple); + let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange); + let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow); + let mut type_option = ChecklistTypeOption::default(); + type_option.options.extend(vec![option1, option2, option3]); + let checklist_field = FieldBuilder::new(field_type.clone(), type_option) + .name("TODO") + .visibility(true) + .build(); + fields.push(checklist_field); + }, + } + } + + // We have many assumptions base on the number of the rows, so do not change the number of the loop. + for i in 0..5 { + let mut row_builder = TestRowBuilder::new(i.into(), fields.clone()); + match i { + 0 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("A"), + FieldType::Number => row_builder.insert_number_cell("1"), + // 1647251762 => Mar 14,2022 + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + }, + FieldType::MultiSelect => row_builder + .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]), + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"), + _ => "".to_owned(), + }; + } + }, + 1 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("B"), + FieldType::Number => row_builder.insert_number_cell("2"), + // 1647251762 => Mar 14,2022 + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + }, + FieldType::MultiSelect => row_builder + .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]), + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + }, + 2 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("C"), + FieldType::Number => row_builder.insert_number_cell("3"), + // 1647251762 => Mar 14,2022 + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + }, + FieldType::MultiSelect => { + row_builder.insert_multi_select_cell(|mut options| vec![options.remove(0)]) + }, + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + FieldType::URL => { + row_builder.insert_url_cell("https://github.com/AppFlowy-IO/AppFlowy") + }, + _ => "".to_owned(), + }; + } + }, + 3 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("DA"), + FieldType::Number => row_builder.insert_number_cell("4"), + FieldType::DateTime => row_builder.insert_date_cell("1668704685"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + }, + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + FieldType::URL => row_builder.insert_url_cell("https://appflowy.io"), + _ => "".to_owned(), + }; + } + }, + 4 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("AE"), + FieldType::Number => row_builder.insert_number_cell(""), + FieldType::DateTime => row_builder.insert_date_cell("1668359085"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(2)) + }, + + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + }, + _ => {}, + } + + let row = row_builder.build(); + rows.push(row); + } + + let view = DatabaseView { + id: gen_database_view_id(), + database_id: gen_database_id(), + name: "".to_string(), + layout: DatabaseLayout::Board, + layout_settings: Default::default(), + filters: vec![], + group_settings: vec![], + sorts: vec![], + row_orders: vec![], + field_orders: vec![], + created_at: 0, + modified_at: 0, + }; + DatabaseData { view, fields, rows } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs new file mode 100644 index 0000000000..9761d232b5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs @@ -0,0 +1,115 @@ +use crate::database::database_editor::TestRowBuilder; +use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; + +use flowy_database2::entities::FieldType; +use flowy_database2::services::field::{FieldBuilder, MultiSelectTypeOption}; +use flowy_database2::services::setting::CalendarLayoutSetting; +use strum::IntoEnumIterator; + +// Calendar unit test mock data +pub fn make_test_calendar() -> DatabaseData { + let mut fields = vec![]; + let mut rows = vec![]; + // text + let text_field = FieldBuilder::from_field_type(FieldType::RichText) + .name("Name") + .visibility(true) + .primary(true) + .build(); + fields.push(text_field); + + // date + let date_field = FieldBuilder::from_field_type(FieldType::DateTime) + .name("Date") + .visibility(true) + .build(); + + let date_field_id = date_field.id.clone(); + fields.push(date_field); + + // multi select + + let type_option = MultiSelectTypeOption::default(); + let multi_select_field = FieldBuilder::new(FieldType::MultiSelect, type_option) + .name("Tags") + .visibility(true) + .build(); + fields.push(multi_select_field); + + let calendar_setting: LayoutSetting = CalendarLayoutSetting::new(date_field_id).into(); + + for i in 0..5 { + let mut row_builder = TestRowBuilder::new(i.into(), fields.clone()); + match i { + 0 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("A"), + FieldType::DateTime => row_builder.insert_date_cell("1678090778"), + _ => "".to_owned(), + }; + } + }, + 1 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("B"), + FieldType::DateTime => row_builder.insert_date_cell("1677917978"), + _ => "".to_owned(), + }; + } + }, + 2 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("C"), + FieldType::DateTime => row_builder.insert_date_cell("1679213978"), + _ => "".to_owned(), + }; + } + }, + 3 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("D"), + FieldType::DateTime => row_builder.insert_date_cell("1678695578"), + _ => "".to_owned(), + }; + } + }, + 4 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("E"), + FieldType::DateTime => row_builder.insert_date_cell("1678695578"), + _ => "".to_owned(), + }; + } + }, + _ => {}, + } + + let row = row_builder.build(); + rows.push(row); + } + let mut layout_settings = LayoutSettings::new(); + layout_settings.insert(DatabaseLayout::Calendar, calendar_setting); + + let view = DatabaseView { + id: gen_database_view_id(), + database_id: gen_database_id(), + name: "".to_string(), + layout: DatabaseLayout::Calendar, + layout_settings, + filters: vec![], + group_settings: vec![], + sorts: vec![], + row_orders: vec![], + field_orders: vec![], + created_at: 0, + modified_at: 0, + }; + + DatabaseData { view, fields, rows } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs new file mode 100644 index 0000000000..9254944216 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -0,0 +1,230 @@ +use crate::database::mock_data::{ + COMPLETED, FACEBOOK, FIRST_THING, GOOGLE, PAUSED, PLANNED, SECOND_THING, THIRD_THING, TWITTER, +}; +use collab_database::database::{gen_database_id, gen_database_view_id, DatabaseData}; + +use collab_database::views::{DatabaseLayout, DatabaseView}; + +use crate::database::database_editor::TestRowBuilder; +use flowy_database2::entities::FieldType; +use flowy_database2::services::field::{ + ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, + NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, + TimeFormat, +}; +use strum::IntoEnumIterator; + +pub fn make_test_grid() -> DatabaseData { + let mut fields = vec![]; + let mut rows = vec![]; + // Iterate through the FieldType to create the corresponding Field. + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => { + let text_field = FieldBuilder::from_field_type(field_type.clone()) + .name("Name") + .visibility(true) + .primary(true) + .build(); + fields.push(text_field); + }, + FieldType::Number => { + // Number + let mut type_option = NumberTypeOption::default(); + type_option.set_format(NumberFormat::USD); + + let number_field = FieldBuilder::new(field_type.clone(), type_option) + .name("Price") + .visibility(true) + .build(); + fields.push(number_field); + }, + FieldType::DateTime => { + // Date + let date_type_option = DateTypeOption { + date_format: DateFormat::US, + time_format: TimeFormat::TwentyFourHour, + include_time: false, + }; + let date_field = FieldBuilder::new(field_type.clone(), date_type_option) + .name("Time") + .visibility(true) + .build(); + fields.push(date_field); + }, + FieldType::SingleSelect => { + // Single Select + let option1 = SelectOption::with_color(COMPLETED, SelectOptionColor::Purple); + let option2 = SelectOption::with_color(PLANNED, SelectOptionColor::Orange); + let option3 = SelectOption::with_color(PAUSED, SelectOptionColor::Yellow); + let mut single_select_type_option = SingleSelectTypeOption::default(); + single_select_type_option + .options + .extend(vec![option1, option2, option3]); + let single_select_field = FieldBuilder::new(field_type.clone(), single_select_type_option) + .name("Status") + .visibility(true) + .build(); + fields.push(single_select_field); + }, + FieldType::MultiSelect => { + // MultiSelect + let option1 = SelectOption::with_color(GOOGLE, SelectOptionColor::Purple); + let option2 = SelectOption::with_color(FACEBOOK, SelectOptionColor::Orange); + let option3 = SelectOption::with_color(TWITTER, SelectOptionColor::Yellow); + let mut type_option = MultiSelectTypeOption::default(); + type_option.options.extend(vec![option1, option2, option3]); + let multi_select_field = FieldBuilder::new(field_type.clone(), type_option) + .name("Platform") + .visibility(true) + .build(); + fields.push(multi_select_field); + }, + FieldType::Checkbox => { + // Checkbox + let checkbox_field = FieldBuilder::from_field_type(field_type.clone()) + .name("is urgent") + .visibility(true) + .build(); + fields.push(checkbox_field); + }, + FieldType::URL => { + // URL + let url = FieldBuilder::from_field_type(field_type.clone()) + .name("link") + .visibility(true) + .build(); + fields.push(url); + }, + FieldType::Checklist => { + let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple); + let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange); + let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow); + let mut type_option = ChecklistTypeOption::default(); + type_option.options.extend(vec![option1, option2, option3]); + let checklist_field = FieldBuilder::new(field_type.clone(), type_option) + .name("TODO") + .visibility(true) + .build(); + fields.push(checklist_field); + }, + } + } + + for i in 0..6 { + let mut row_builder = TestRowBuilder::new(i.into(), fields.clone()); + match i { + 0 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("A"), + FieldType::Number => row_builder.insert_number_cell("1"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::MultiSelect => row_builder + .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]), + FieldType::Checklist => row_builder.insert_checklist_cell(|options| options), + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + FieldType::URL => { + row_builder.insert_url_cell("AppFlowy website - https://www.appflowy.io") + }, + _ => "".to_owned(), + }; + } + }, + 1 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell(""), + FieldType::Number => row_builder.insert_number_cell("2"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::MultiSelect => row_builder + .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(1)]), + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + }, + 2 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("C"), + FieldType::Number => row_builder.insert_number_cell("3"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + }, + FieldType::MultiSelect => { + row_builder.insert_multi_select_cell(|mut options| vec![options.remove(1)]) + }, + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + }, + 3 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("DA"), + FieldType::Number => row_builder.insert_number_cell("4"), + FieldType::DateTime => row_builder.insert_date_cell("1668704685"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + }, + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + }, + 4 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("AE"), + FieldType::Number => row_builder.insert_number_cell(""), + FieldType::DateTime => row_builder.insert_date_cell("1668359085"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + }, + + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + }, + 5 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("AE"), + FieldType::Number => row_builder.insert_number_cell("5"), + FieldType::DateTime => row_builder.insert_date_cell("1671938394"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + }, + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + }, + _ => {}, + } + + let row = row_builder.build(); + rows.push(row); + } + + let view = DatabaseView { + id: gen_database_view_id(), + database_id: gen_database_id(), + name: "".to_string(), + layout: DatabaseLayout::Grid, + layout_settings: Default::default(), + filters: vec![], + group_settings: vec![], + sorts: vec![], + row_orders: vec![], + field_orders: vec![], + created_at: 0, + modified_at: 0, + }; + + DatabaseData { view, fields, rows } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/mod.rs new file mode 100644 index 0000000000..6b47039952 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/mod.rs @@ -0,0 +1,19 @@ +mod board_mock_data; +mod calendar_mock_data; +mod grid_mock_data; + +pub use board_mock_data::*; +pub use calendar_mock_data::*; +pub use grid_mock_data::*; + +pub const GOOGLE: &str = "Google"; +pub const FACEBOOK: &str = "Facebook"; +pub const TWITTER: &str = "Twitter"; + +pub const COMPLETED: &str = "Completed"; +pub const PLANNED: &str = "Planned"; +pub const PAUSED: &str = "Paused"; + +pub const FIRST_THING: &str = "Wake up at 6:00 am"; +pub const SECOND_THING: &str = "Get some coffee"; +pub const THIRD_THING: &str = "Start working"; diff --git a/frontend/rust-lib/flowy-database2/tests/database/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/mod.rs new file mode 100644 index 0000000000..0e00cd5769 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/mod.rs @@ -0,0 +1,9 @@ +mod cell_test; +mod database_editor; +mod field_test; +mod filter_test; +mod group_test; +mod layout_test; +mod sort_test; + +mod mock_data; diff --git a/frontend/rust-lib/flowy-database2/tests/database/script.rs b/frontend/rust-lib/flowy-database2/tests/database/script.rs new file mode 100644 index 0000000000..69511759c0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/script.rs @@ -0,0 +1,425 @@ +use bytes::Bytes; +use database_model::entities::{ + BuildGridContext, CellChangeset, Field, FieldChangesetParams, FieldMeta, FieldOrder, FieldType, + GridBlockInfoChangeset, GridBlockMetaSnapshot, InsertFieldParams, RowMeta, RowMetaChangeset, + RowOrder, TypeOptionDataFormat, +}; +use flowy_client_sync::client_grid::GridBuilder; +use flowy_database::services::field::*; +use flowy_database::services::grid_meta_editor::{GridMetaEditor, GridPadBuilder}; +use flowy_database::services::row::CreateRowMetaPayload; +use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; +use flowy_test::helper::ViewTest; +use flowy_test::FlowySDKTest; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use strum::EnumCount; +use tokio::time::sleep; + +pub enum EditorScript { + CreateField { + params: InsertFieldParams, + }, + UpdateField { + changeset: FieldChangesetParams, + }, + DeleteField { + field_meta: FieldMeta, + }, + AssertFieldCount(usize), + AssertFieldEqual { + field_index: usize, + field_meta: FieldMeta, + }, + CreateBlock { + block: GridBlockMetaSnapshot, + }, + UpdateBlock { + changeset: GridBlockInfoChangeset, + }, + AssertBlockCount(usize), + AssertBlock { + block_index: usize, + row_count: i32, + start_row_index: i32, + }, + AssertBlockEqual { + block_index: usize, + block: GridBlockMetaSnapshot, + }, + CreateEmptyRow, + CreateRow { + context: CreateRowMetaPayload, + }, + UpdateRow { + changeset: RowMetaChangeset, + }, + AssertRow { + changeset: RowMetaChangeset, + }, + DeleteRow { + row_ids: Vec, + }, + UpdateCell { + changeset: CellChangeset, + is_err: bool, + }, + AssertRowCount(usize), + // AssertRowEqual{ row_index: usize, row: RowMeta}, + AssertGridMetaPad, +} + +pub struct GridEditorTest { + pub sdk: FlowySDKTest, + pub grid_id: String, + pub editor: Arc, + pub field_metas: Vec, + pub grid_blocks: Vec, + pub row_metas: Vec>, + pub field_count: usize, + + pub row_order_by_row_id: HashMap, +} + +impl GridEditorTest { + pub async fn new() -> Self { + let sdk = FlowySDKTest::default(); + let _ = sdk.init_user().await; + let build_context = make_template_1_grid(); + let view_data: Bytes = build_context.into(); + let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await; + let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap(); + let field_metas = editor.get_field_metas::(None).await.unwrap(); + let grid_blocks = editor.get_block_metas().await.unwrap(); + let row_metas = get_row_metas(&editor).await; + + let grid_id = test.view.id; + Self { + sdk, + grid_id, + editor, + field_metas, + grid_blocks, + row_metas, + field_count: FieldType::COUNT, + row_order_by_row_id: HashMap::default(), + } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: EditorScript) { + let grid_manager = self.sdk.grid_manager.clone(); + let pool = self.sdk.user_session.db_pool().unwrap(); + let rev_manager = self.editor.rev_manager(); + let _cache = rev_manager.revision_cache().await; + + match script { + EditorScript::CreateField { params } => { + if !self.editor.contain_field(¶ms.field.id).await { + self.field_count += 1; + } + + self.editor.insert_field(params).await.unwrap(); + self.field_metas = self + .editor + .get_field_metas::(None) + .await + .unwrap(); + assert_eq!(self.field_count, self.field_metas.len()); + }, + EditorScript::UpdateField { changeset: change } => { + self.editor.update_field(change).await.unwrap(); + self.field_metas = self + .editor + .get_field_metas::(None) + .await + .unwrap(); + }, + EditorScript::DeleteField { field_meta } => { + if self.editor.contain_field(&field_meta.id).await { + self.field_count -= 1; + } + + self.editor.delete_field(&field_meta.id).await.unwrap(); + self.field_metas = self + .editor + .get_field_metas::(None) + .await + .unwrap(); + assert_eq!(self.field_count, self.field_metas.len()); + }, + EditorScript::AssertFieldCount(count) => { + assert_eq!( + self + .editor + .get_field_metas::(None) + .await + .unwrap() + .len(), + count + ); + }, + EditorScript::AssertFieldEqual { + field_index, + field_meta, + } => { + let field_metas = self + .editor + .get_field_metas::(None) + .await + .unwrap(); + assert_eq!(field_metas[field_index].clone(), field_meta); + }, + EditorScript::CreateBlock { block } => { + self.editor.create_block(block).await.unwrap(); + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); + }, + EditorScript::UpdateBlock { changeset: change } => { + self.editor.update_block(change).await.unwrap(); + }, + EditorScript::AssertBlockCount(count) => { + assert_eq!(self.editor.get_block_metas().await.unwrap().len(), count); + }, + EditorScript::AssertBlock { + block_index, + row_count, + start_row_index, + } => { + assert_eq!(self.grid_blocks[block_index].row_count, row_count); + assert_eq!( + self.grid_blocks[block_index].start_row_index, + start_row_index + ); + }, + EditorScript::AssertBlockEqual { block_index, block } => { + let blocks = self.editor.get_block_metas().await.unwrap(); + let compared_block = blocks[block_index].clone(); + assert_eq!(compared_block, block); + }, + EditorScript::CreateEmptyRow => { + let row_order = self.editor.create_row(None).await.unwrap(); + self + .row_order_by_row_id + .insert(row_order.row_id.clone(), row_order); + self.row_metas = self.get_row_metas().await; + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); + }, + EditorScript::CreateRow { context } => { + let row_orders = self.editor.insert_rows(vec![context]).await.unwrap(); + for row_order in row_orders { + self + .row_order_by_row_id + .insert(row_order.row_id.clone(), row_order); + } + self.row_metas = self.get_row_metas().await; + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); + }, + EditorScript::UpdateRow { changeset: change } => { + self.editor.update_row(change).await.unwrap() + }, + EditorScript::DeleteRow { row_ids } => { + let row_orders = row_ids + .into_iter() + .map(|row_id| self.row_order_by_row_id.get(&row_id).unwrap().clone()) + .collect::>(); + + self.editor.delete_rows(row_orders).await.unwrap(); + self.row_metas = self.get_row_metas().await; + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); + }, + EditorScript::AssertRow { changeset } => { + let row = self + .row_metas + .iter() + .find(|row| row.id == changeset.row_id) + .unwrap(); + + if let Some(visibility) = changeset.visibility { + assert_eq!(row.visibility, visibility); + } + + if let Some(height) = changeset.height { + assert_eq!(row.height, height); + } + }, + EditorScript::UpdateCell { changeset, is_err } => { + let result = self.editor.update_cell(changeset).await; + if is_err { + assert!(result.is_err()) + } else { + let _ = result.unwrap(); + self.row_metas = self.get_row_metas().await; + } + }, + EditorScript::AssertRowCount(count) => { + assert_eq!(self.row_metas.len(), count); + }, + EditorScript::AssertGridMetaPad => { + sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await; + let mut grid_rev_manager = grid_manager + .make_grid_rev_manager(&self.grid_id, pool.clone()) + .unwrap(); + let grid_pad = grid_rev_manager.load::(None).await.unwrap(); + println!("{}", grid_pad.delta_str()); + }, + } + } + + async fn get_row_metas(&self) -> Vec> { + get_row_metas(&self.editor).await + } +} + +async fn get_row_metas(editor: &Arc) -> Vec> { + editor + .grid_block_snapshots(None) + .await + .unwrap() + .pop() + .unwrap() + .row_metas +} + +pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) { + let field_meta = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Name") + .visibility(true) + .build(); + + let cloned_field_meta = field_meta.clone(); + + let type_option_data = field_meta + .get_type_option_entry::(&field_meta.field_type) + .unwrap() + .protobuf_bytes() + .to_vec(); + + let field = Field { + id: field_meta.id, + name: field_meta.name, + desc: field_meta.desc, + field_type: field_meta.field_type, + frozen: field_meta.frozen, + visibility: field_meta.visibility, + width: field_meta.width, + is_primary: false, + }; + + let params = InsertFieldParams { + grid_id: grid_id.to_owned(), + field, + type_option_data, + start_field_id: None, + }; + (params, cloned_field_meta) +} + +pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) { + let single_select = SingleSelectTypeOptionBuilder::default() + .option(SelectOption::new("Done")) + .option(SelectOption::new("Progress")); + + let field_meta = FieldBuilder::new(single_select) + .name("Name") + .visibility(true) + .build(); + let cloned_field_meta = field_meta.clone(); + let type_option_data = field_meta + .get_type_option_entry::(&field_meta.field_type) + .unwrap() + .protobuf_bytes() + .to_vec(); + + let field = Field { + id: field_meta.id, + name: field_meta.name, + desc: field_meta.desc, + field_type: field_meta.field_type, + frozen: field_meta.frozen, + visibility: field_meta.visibility, + width: field_meta.width, + is_primary: false, + }; + + let params = InsertFieldParams { + grid_id: grid_id.to_owned(), + field, + type_option_data, + start_field_id: None, + }; + (params, cloned_field_meta) +} + +fn make_template_1_grid() -> BuildGridContext { + let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Name") + .visibility(true) + .build(); + + // Single Select + let single_select = SingleSelectTypeOptionBuilder::default() + .option(SelectOption::new("Live")) + .option(SelectOption::new("Completed")) + .option(SelectOption::new("Planned")) + .option(SelectOption::new("Paused")); + let single_select_field = FieldBuilder::new(single_select) + .name("Status") + .visibility(true) + .build(); + + // MultiSelect + let multi_select = MultiSelectTypeOptionBuilder::default() + .option(SelectOption::new("Google")) + .option(SelectOption::new("Facebook")) + .option(SelectOption::new("Twitter")); + let multi_select_field = FieldBuilder::new(multi_select) + .name("Platform") + .visibility(true) + .build(); + + // Number + let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD); + let number_field = FieldBuilder::new(number) + .name("Price") + .visibility(true) + .build(); + + // Date + let date = DateTypeOptionBuilder::default() + .date_format(DateFormat::US) + .time_format(TimeFormat::TwentyFourHour); + let date_field = FieldBuilder::new(date) + .name("Time") + .visibility(true) + .build(); + + // Checkbox + let checkbox = CheckboxTypeOptionBuilder::default(); + let checkbox_field = FieldBuilder::new(checkbox) + .name("is done") + .visibility(true) + .build(); + + // URL + let url = URLTypeOptionBuilder::default(); + let url_field = FieldBuilder::new(url).name("link").visibility(true).build(); + + GridBuilder::default() + .add_field(text_field) + .add_field(single_select_field) + .add_field(multi_select_field) + .add_field(number_field) + .add_field(date_field) + .add_field(checkbox_field) + .add_field(url_field) + .add_empty_row() + .add_empty_row() + .add_empty_row() + .build() +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/checkbox_and_text_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/checkbox_and_text_test.rs new file mode 100644 index 0000000000..9a2294792a --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/checkbox_and_text_test.rs @@ -0,0 +1,52 @@ +use flowy_database2::entities::FieldType; +use flowy_database2::services::sort::SortCondition; + +use crate::database::sort_test::script::DatabaseSortTest; +use crate::database::sort_test::script::SortScript::{AssertCellContentOrder, InsertSort}; + +#[tokio::test] +async fn sort_checkbox_and_then_text_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE"], + }, + // // Insert checkbox sort + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "AE", "C", "DA", "AE"], + }, + // Insert text sort. After inserting the text sort, the order of the rows + // will be changed. + // before: ["A", "", "AE", "C", "DA", "AE"] + // after: ["", "A", "AE", "AE", "C", "DA"] + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["", "A", "AE", "AE", "C", "DA"], + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/mod.rs new file mode 100644 index 0000000000..69e0a622f8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/mod.rs @@ -0,0 +1,4 @@ +mod checkbox_and_text_test; +mod multi_sort_test; +mod script; +mod single_sort_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs new file mode 100644 index 0000000000..8977aa5829 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs @@ -0,0 +1,47 @@ +use flowy_database2::entities::FieldType; +use flowy_database2::services::sort::SortCondition; + +use crate::database::sort_test::script::DatabaseSortTest; +use crate::database::sort_test::script::SortScript::*; + +#[tokio::test] +async fn sort_text_with_checkbox_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let text_field = test.get_first_field(FieldType::RichText).clone(); + let checkbox_field = test.get_first_field(FieldType::Checkbox).clone(); + let scripts = vec![ + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE"], + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No"], + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["", "A", "AE", "AE", "C", "DA"], + }, + ]; + test.run_scripts(scripts).await; + + let scripts = vec![ + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["", "A", "AE", "AE", "C", "DA"], + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No"], + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs new file mode 100644 index 0000000000..102fa9fb13 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs @@ -0,0 +1,199 @@ +use std::cmp::min; + +use std::time::Duration; + +use async_stream::stream; +use collab_database::fields::Field; +use collab_database::rows::RowId; +use futures::stream::StreamExt; +use tokio::sync::broadcast::Receiver; + +use flowy_database2::entities::{AlterSortParams, DeleteSortParams, FieldType}; +use flowy_database2::services::cell::stringify_cell_data; +use flowy_database2::services::database_view::DatabaseViewChanged; +use flowy_database2::services::sort::{Sort, SortCondition, SortType}; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum SortScript { + InsertSort { + field: Field, + condition: SortCondition, + }, + DeleteSort { + sort: Sort, + sort_id: String, + }, + AssertCellContentOrder { + field_id: String, + orders: Vec<&'static str>, + }, + UpdateTextCell { + row_id: RowId, + text: String, + }, + AssertSortChanged { + old_row_orders: Vec<&'static str>, + new_row_orders: Vec<&'static str>, + }, + Wait { + millis: u64, + }, +} + +pub struct DatabaseSortTest { + inner: DatabaseEditorTest, + pub current_sort_rev: Option, + recv: Option>, +} + +impl DatabaseSortTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { + inner: editor_test, + current_sort_rev: None, + recv: None, + } + } + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: SortScript) { + match script { + SortScript::InsertSort { condition, field } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + let params = AlterSortParams { + view_id: self.view_id.clone(), + field_id: field.id.clone(), + sort_id: None, + field_type: FieldType::from(field.field_type), + condition, + }; + let sort_rev = self.editor.create_or_update_sort(params).await.unwrap(); + self.current_sort_rev = Some(sort_rev); + }, + SortScript::DeleteSort { sort, sort_id } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + let params = DeleteSortParams { + view_id: self.view_id.clone(), + sort_type: SortType::from(&sort), + sort_id, + }; + self.editor.delete_sort(params).await.unwrap(); + self.current_sort_rev = None; + }, + SortScript::AssertCellContentOrder { field_id, orders } => { + let mut cells = vec![]; + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + for row in rows { + if let Some(cell) = row.cells.get(&field_id) { + let content = stringify_cell_data(cell, &field_type, &field_type, &field); + cells.push(content); + } else { + cells.push("".to_string()); + } + } + if orders.is_empty() { + assert_eq!(cells, orders); + } else { + let len = min(cells.len(), orders.len()); + assert_eq!(cells.split_at(len).0, orders); + } + }, + SortScript::UpdateTextCell { row_id, text } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + self.update_text_cell(row_id, &text).await; + }, + SortScript::AssertSortChanged { + new_row_orders, + old_row_orders, + } => { + if let Some(receiver) = self.recv.take() { + assert_sort_changed( + receiver, + new_row_orders + .into_iter() + .map(|order| order.to_owned()) + .collect(), + old_row_orders + .into_iter() + .map(|order| order.to_owned()) + .collect(), + ) + .await; + } + }, + SortScript::Wait { millis } => { + tokio::time::sleep(Duration::from_millis(millis)).await; + }, + } + } +} + +async fn assert_sort_changed( + mut receiver: Receiver, + new_row_orders: Vec, + old_row_orders: Vec, +) { + let stream = stream! { + loop { + tokio::select! { + changed = receiver.recv() => yield changed.unwrap(), + _ = tokio::time::sleep(Duration::from_secs(2)) => break, + }; + } + }; + + stream + .for_each(|changed| async { + match changed { + DatabaseViewChanged::ReorderAllRowsNotification(_changed) => {}, + DatabaseViewChanged::ReorderSingleRowNotification(changed) => { + let mut old_row_orders = old_row_orders.clone(); + let old = old_row_orders.remove(changed.old_index); + old_row_orders.insert(changed.new_index, old); + assert_eq!(old_row_orders, new_row_orders); + }, + _ => {}, + } + }) + .await; +} + +impl std::ops::Deref for DatabaseSortTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for DatabaseSortTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs new file mode 100644 index 0000000000..6a751808d8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs @@ -0,0 +1,270 @@ +use flowy_database2::entities::FieldType; +use flowy_database2::services::sort::SortCondition; + +use crate::database::sort_test::script::{DatabaseSortTest, SortScript::*}; + +#[tokio::test] +async fn sort_text_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE"], + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["", "A", "AE", "AE", "C", "DA"], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_change_notification_by_update_text_test() { + let mut test = DatabaseSortTest::new().await; + let text_field = test.get_first_field(FieldType::RichText).clone(); + let scripts = vec![ + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["", "A", "AE", "AE", "C", "DA"], + }, + // Wait the insert task to finish. The cost of time should be less than 200 milliseconds. + Wait { millis: 200 }, + ]; + test.run_scripts(scripts).await; + + let rows = test.get_rows().await; + let scripts = vec![ + UpdateTextCell { + row_id: rows[2].id, + text: "E".to_string(), + }, + AssertSortChanged { + old_row_orders: vec!["", "A", "E", "AE", "C", "DA"], + new_row_orders: vec!["", "A", "AE", "C", "DA", "E"], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_text_by_ascending_and_delete_sort_test() { + let mut test = DatabaseSortTest::new().await; + let text_field = test.get_first_field(FieldType::RichText).clone(); + let scripts = vec![InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }]; + test.run_scripts(scripts).await; + let sort = test.current_sort_rev.as_ref().unwrap(); + let scripts = vec![ + DeleteSort { + sort: sort.clone(), + sort_id: sort.id.clone(), + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE"], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_text_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE"], + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["DA", "C", "AE", "AE", "A", ""], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_checkbox_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No"], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Ascending, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_checkbox_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_date_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let date_field = test.get_first_field(FieldType::DateTime); + let scripts = vec![ + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ + "2022/03/14", + "2022/03/14", + "2022/03/14", + "2022/11/17", + "2022/11/13", + ], + }, + InsertSort { + field: date_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ + "2022/03/14", + "2022/03/14", + "2022/03/14", + "2022/11/13", + "2022/11/17", + ], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_date_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let date_field = test.get_first_field(FieldType::DateTime); + let scripts = vec![ + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ + "2022/03/14", + "2022/03/14", + "2022/03/14", + "2022/11/17", + "2022/11/13", + "2022/12/25", + ], + }, + InsertSort { + field: date_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ + "2022/12/25", + "2022/11/17", + "2022/11/13", + "2022/03/14", + "2022/03/14", + "2022/03/14", + ], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_number_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let number_field = test.get_first_field(FieldType::Number); + let scripts = vec![ + AssertCellContentOrder { + field_id: number_field.id.clone(), + orders: vec!["$1", "$2", "$3", "$4", "", "$5"], + }, + InsertSort { + field: number_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: number_field.id.clone(), + orders: vec!["$5", "$4", "$3", "$2", "$1", ""], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_single_select_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let single_select = test.get_first_field(FieldType::SingleSelect); + let scripts = vec![ + AssertCellContentOrder { + field_id: single_select.id.clone(), + orders: vec!["", "", "Completed", "Completed", "Planned", "Planned"], + }, + InsertSort { + field: single_select.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: single_select.id.clone(), + orders: vec!["Planned", "Planned", "Completed", "Completed", "", ""], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_multi_select_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let multi_select = test.get_first_field(FieldType::MultiSelect); + let scripts = vec![ + AssertCellContentOrder { + field_id: multi_select.id.clone(), + orders: vec!["Google,Facebook", "Google,Twitter", "Facebook", "", "", ""], + }, + InsertSort { + field: multi_select.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: multi_select.id.clone(), + orders: vec!["", "", "", "Facebook", "Google,Facebook", "Google,Twitter"], + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/main.rs b/frontend/rust-lib/flowy-database2/tests/main.rs new file mode 100644 index 0000000000..3a9960ec68 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/main.rs @@ -0,0 +1 @@ +mod database; diff --git a/frontend/rust-lib/flowy-derive/src/proto_buf/deserialize.rs b/frontend/rust-lib/flowy-derive/src/proto_buf/deserialize.rs index 0762ae3e6f..d719ec5477 100644 --- a/frontend/rust-lib/flowy-derive/src/proto_buf/deserialize.rs +++ b/frontend/rust-lib/flowy-derive/src/proto_buf/deserialize.rs @@ -1,7 +1,9 @@ -use crate::proto_buf::util::*; -use flowy_ast::*; use proc_macro2::{Span, TokenStream}; +use flowy_ast::*; + +use crate::proto_buf::util::*; + pub fn make_de_token_steam(ast_result: &ASTResult, ast: &ASTContainer) -> Option { let pb_ty = ast.pb_attrs.pb_struct_type()?; let struct_ident = &ast.ident; @@ -225,13 +227,18 @@ fn token_stream_for_vec( o.#member = pb.#member.clone(); }) }, - _ => { - // String + TypeCategory::Str => { let take_ident = format_ident!("take_{}", ident.to_string()); Some(quote! { o.#member = pb.#take_ident().into_vec(); }) }, + _ => { + let take_ident = format_ident!("take_{}", ident.to_string()); + Some(quote! { + o.#member = pb.#take_ident(); + }) + }, } } diff --git a/frontend/rust-lib/flowy-derive/src/proto_buf/serialize.rs b/frontend/rust-lib/flowy-derive/src/proto_buf/serialize.rs index 4e1ee2d6ea..b44e192576 100644 --- a/frontend/rust-lib/flowy-derive/src/proto_buf/serialize.rs +++ b/frontend/rust-lib/flowy-derive/src/proto_buf/serialize.rs @@ -1,8 +1,11 @@ #![allow(clippy::while_let_on_iterator)] -use crate::proto_buf::util::{get_member_ident, ident_category, TypeCategory}; -use flowy_ast::*; + use proc_macro2::TokenStream; +use flowy_ast::*; + +use crate::proto_buf::util::{get_member_ident, ident_category, TypeCategory}; + pub fn make_se_token_stream(ast_result: &ASTResult, ast: &ASTContainer) -> Option { let pb_ty = ast.pb_attrs.pb_struct_type()?; let struct_ident = &ast.ident; @@ -172,7 +175,9 @@ fn token_stream_for_vec( .collect()); }), TypeCategory::Bytes => Some(quote! { pb.#member = o.#member.clone(); }), - + TypeCategory::Primitive => Some(quote! { + pb.#member = o.#member.clone(); + }), _ => Some(quote! { pb.#member = ::protobuf::RepeatedField::from_vec(o.#member.clone()); }), diff --git a/frontend/rust-lib/flowy-document2/src/event_map.rs b/frontend/rust-lib/flowy-document2/src/event_map.rs index 6c1611bc40..acea8ca0c0 100644 --- a/frontend/rust-lib/flowy-document2/src/event_map.rs +++ b/frontend/rust-lib/flowy-document2/src/event_map.rs @@ -5,7 +5,9 @@ use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; use crate::{ - event_handler::{apply_action_handler, close_document_handler, open_document_handler, create_document_handler }, + event_handler::{ + apply_action_handler, close_document_handler, create_document_handler, open_document_handler, + }, manager::DocumentManager, }; diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 6e1ed88b3d..0645972456 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -1,10 +1,11 @@ use collab::plugin_impl::rocks_disk::RocksDiskPlugin; use collab::preclude::{Collab, CollabBuilder}; use collab_persistence::kv::rocks_kv::RocksCollabDB; -use flowy_error::{FlowyError, FlowyResult}; use parking_lot::RwLock; use std::{collections::HashMap, sync::Arc}; +use flowy_error::{FlowyError, FlowyResult}; + use crate::{ document::{Document, DocumentDataWrapper}, entities::DocEventPB, diff --git a/frontend/rust-lib/flowy-document2/src/notification.rs b/frontend/rust-lib/flowy-document2/src/notification.rs index 65ab6d62dc..44cc657a01 100644 --- a/frontend/rust-lib/flowy-document2/src/notification.rs +++ b/frontend/rust-lib/flowy-document2/src/notification.rs @@ -1,7 +1,6 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; - const OBSERVABLE_CATEGORY: &str = "Document"; #[derive(ProtoBuf_Enum, Debug)] diff --git a/frontend/rust-lib/flowy-document2/tests/document_test.rs b/frontend/rust-lib/flowy-document2/tests/document_test.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/document_test.rs @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index 8b53aead41..fd9ebd08d7 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -24,6 +24,7 @@ reqwest = { version = "0.11.14", optional = true } http-error-code = { git = "https://github.com/AppFlowy-IO/AppFlowy-Server", branch = "refactor/appflowy_server", optional = true } flowy-sqlite = { path = "../flowy-sqlite", optional = true} r2d2 = { version = "0.8", optional = true} +collab-database = { version = "0.1.0", optional = true } [features] adaptor_sync = ["flowy-client-sync"] @@ -37,6 +38,7 @@ adaptor_user= ["user-model"] adaptor_server_error = ["http-error-code"] dart = ["flowy-codegen/dart"] ts = ["flowy-codegen/ts"] +collab = ["collab-database"] [build-dependencies] flowy-codegen = { path = "../flowy-codegen", features = ["proto_gen"]} diff --git a/frontend/rust-lib/flowy-error/src/ext/collab.rs b/frontend/rust-lib/flowy-error/src/ext/collab.rs new file mode 100644 index 0000000000..5b7e648501 --- /dev/null +++ b/frontend/rust-lib/flowy-error/src/ext/collab.rs @@ -0,0 +1,8 @@ +use crate::FlowyError; +use collab_database::error::DatabaseError; + +impl From for FlowyError { + fn from(error: DatabaseError) -> Self { + FlowyError::internal().context(error) + } +} diff --git a/frontend/rust-lib/flowy-error/src/ext/mod.rs b/frontend/rust-lib/flowy-error/src/ext/mod.rs index 89a7000b63..901bd707e4 100644 --- a/frontend/rust-lib/flowy-error/src/ext/mod.rs +++ b/frontend/rust-lib/flowy-error/src/ext/mod.rs @@ -24,3 +24,6 @@ pub mod user; #[cfg(feature = "adaptor_server_error")] pub mod http_server; + +#[cfg(feature = "collab")] +pub mod collab; diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 8fa9a83b32..0acde3c53e 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -1,3 +1,20 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::sync::Arc; + +use collab::plugin_impl::rocks_disk::RocksDiskPlugin; +use collab::preclude::CollabBuilder; +use collab_folder::core::{ + Folder as InnerFolder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, TrashRecord, + View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace, +}; +use collab_persistence::kv::rocks_kv::RocksCollabDB; +use parking_lot::Mutex; +use tracing::{event, Level}; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_infra::util::timestamp; + use crate::entities::{ CreateViewParams, CreateWorkspaceParams, RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, ViewPB, @@ -10,20 +27,6 @@ use crate::user_default::{gen_workspace_id, DefaultFolderBuilder}; use crate::view_ext::{ gen_view_id, view_from_create_view_params, ViewDataProcessor, ViewDataProcessorMap, }; -use collab::plugin_impl::rocks_disk::RocksDiskPlugin; -use collab::preclude::CollabBuilder; -use collab_folder::core::{ - Folder as InnerFolder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, TrashRecord, - View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace, -}; -use collab_persistence::kv::rocks_kv::RocksCollabDB; -use flowy_error::{FlowyError, FlowyResult}; -use lib_infra::util::timestamp; -use parking_lot::Mutex; -use std::collections::{HashMap, HashSet}; -use std::ops::Deref; -use std::sync::Arc; -use tracing::{event, Level}; pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result; @@ -85,34 +88,36 @@ impl Folder2Manager { Ok(views) } - /// Called immediately after the application launched with the user sign in/sign up. + /// Called immediately after the application launched fi the user already sign in/sign up. #[tracing::instrument(level = "trace", skip(self), err)] pub async fn initialize(&self, user_id: i64) -> FlowyResult<()> { if let Ok(uid) = self.user.user_id() { let folder_id = FolderId::new(uid); - let mut collab = CollabBuilder::new(uid, folder_id).build(); + if let Ok(kv_db) = self.user.kv_db() { + let mut collab = CollabBuilder::new(uid, folder_id).build(); let disk_plugin = Arc::new( RocksDiskPlugin::new(uid, kv_db).map_err(|err| FlowyError::internal().context(err))?, ); collab.add_plugin(disk_plugin); collab.initial(); - } - let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); - let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); - let folder_context = FolderContext { - view_change_tx: Some(view_tx), - trash_change_tx: Some(trash_tx), - }; - *self.folder.lock() = Some(InnerFolder::get_or_create(collab, folder_context)); - listen_on_trash_change(trash_rx, self.folder.clone()); - listen_on_view_change(view_rx, self.folder.clone()); + let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); + let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); + let folder_context = FolderContext { + view_change_tx: Some(view_tx), + trash_change_tx: Some(trash_tx), + }; + *self.folder.lock() = Some(InnerFolder::get_or_create(collab, folder_context)); + listen_on_trash_change(trash_rx, self.folder.clone()); + listen_on_view_change(view_rx, self.folder.clone()); + } } Ok(()) } + /// Called after the user sign up / sign in pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> { self.initialize(user_id).await?; let (folder_data, workspace_pb) = @@ -131,8 +136,7 @@ impl Folder2Manager { /// Called when the current user logout /// - pub async fn clear(&self, _user_id: i64) { - } + pub async fn clear(&self, _user_id: i64) {} pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult { let workspace = Workspace { diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 643c6cabd6..5b70cf9164 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -189,21 +189,21 @@ diesel::table! { } diesel::allow_tables_to_appear_in_same_query!( - app_table, - database_refs, - document_rev_snapshot, - document_rev_table, - folder_rev_snapshot, - grid_block_index_table, - grid_meta_rev_table, - grid_rev_snapshot, - grid_rev_table, - grid_view_rev_table, - kv_table, - rev_snapshot, - rev_table, - trash_table, - user_table, - view_table, - workspace_table, + app_table, + database_refs, + document_rev_snapshot, + document_rev_table, + folder_rev_snapshot, + grid_block_index_table, + grid_meta_rev_table, + grid_rev_snapshot, + grid_rev_table, + grid_view_rev_table, + kv_table, + rev_snapshot, + rev_table, + trash_table, + user_table, + view_table, + workspace_table, ); diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs index 8ab53ac979..3858804b1d 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Duration}; lazy_static::lazy_static! { static ref DB_POOL: Arc = Arc::new( - ScheduledThreadPool::with_name("db-pool-{}:", 4) + ScheduledThreadPool::builder().num_threads(4).thread_name_pattern("db-pool-{}:").build() ); } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index d6fcaaa089..cf9b93d9f0 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -1,11 +1,13 @@ +use std::path::PathBuf; +use std::{collections::HashMap, sync::Arc, time::Duration}; + use collab_persistence::kv::rocks_kv::RocksCollabDB; +use lazy_static::lazy_static; +use parking_lot::RwLock; + use flowy_error::FlowyError; use flowy_sqlite::ConnectionPool; use flowy_sqlite::{schema::user_table, DBConnection, Database}; -use lazy_static::lazy_static; -use parking_lot::RwLock; -use std::path::PathBuf; -use std::{collections::HashMap, sync::Arc, time::Duration}; use user_model::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; pub struct UserDB { @@ -65,10 +67,10 @@ impl UserDB { tracing::trace!("open kv db {} at path: {:?}", user_id, dir); let db = RocksCollabDB::open(dir).map_err(|err| FlowyError::internal().context(err))?; - let kv_db = Arc::new(db); - write_guard.insert(user_id.to_owned(), kv_db.clone()); + let db = Arc::new(db); + write_guard.insert(user_id.to_owned(), db.clone()); drop(write_guard); - Ok(kv_db) + Ok(db) } pub(crate) fn close_user_db(&self, user_id: i64) -> Result<(), FlowyError> { @@ -133,19 +135,19 @@ impl UserTable { } } -impl std::convert::From for UserTable { +impl From for UserTable { fn from(resp: SignUpResponse) -> Self { UserTable::new(resp.user_id.to_string(), resp.name, resp.email, resp.token) } } -impl std::convert::From for UserTable { +impl From for UserTable { fn from(resp: SignInResponse) -> Self { UserTable::new(resp.user_id.to_string(), resp.name, resp.email, resp.token) } } -impl std::convert::From for UserProfile { +impl From for UserProfile { fn from(table: UserTable) -> Self { UserProfile { id: table.id.parse::().unwrap_or(0), diff --git a/frontend/rust-lib/flowy-user/src/services/user_session.rs b/frontend/rust-lib/flowy-user/src/services/user_session.rs index 17a450772b..b7983a4a6a 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_session.rs @@ -1,12 +1,5 @@ -use crate::entities::{UserProfilePB, UserSettingPB}; -use crate::event_map::UserStatusCallback; +use std::sync::Arc; -use crate::{ - errors::{ErrorCode, FlowyError}, - event_map::UserCloudService, - notification::*, - services::database::{UserDB, UserTable, UserTableChangeset}, -}; use collab_persistence::kv::rocks_kv::RocksCollabDB; use flowy_sqlite::ConnectionPool; use flowy_sqlite::{ @@ -15,14 +8,21 @@ use flowy_sqlite::{ schema::{user_table, user_table::dsl}, DBConnection, ExpressionMethods, UserDatabaseConnection, }; - use serde::{Deserialize, Serialize}; -use std::sync::Arc; use tokio::sync::RwLock; use user_model::{ SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, }; +use crate::entities::{UserProfilePB, UserSettingPB}; +use crate::event_map::UserStatusCallback; +use crate::{ + errors::{ErrorCode, FlowyError}, + event_map::UserCloudService, + notification::*, + services::database::{UserDB, UserTable, UserTableChangeset}, +}; + // lazy_static! { // static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); // }