From 65e41dca88c7858db3d26a424dadef4483c68783 Mon Sep 17 00:00:00 2001 From: Ian Su Date: Mon, 1 Aug 2022 09:13:32 +0800 Subject: [PATCH 001/224] feat: pick icon in settings dialog --- .../app_flowy/assets/images/emoji/close.svg | 4 + .../assets/images/emoji/favorite_active.svg | 5 + .../images/emoji/favorite_inacvtive.svg | 5 + .../app_flowy/assets/images/emoji/image.svg | 6 + .../app_flowy/assets/images/emoji/page.svg | 6 + .../settings/widgets/settings_user_view.dart | 114 +++++++++++++++++- frontend/app_flowy/pubspec.yaml | 1 + 7 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/assets/images/emoji/close.svg create mode 100644 frontend/app_flowy/assets/images/emoji/favorite_active.svg create mode 100644 frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg create mode 100644 frontend/app_flowy/assets/images/emoji/image.svg create mode 100644 frontend/app_flowy/assets/images/emoji/page.svg diff --git a/frontend/app_flowy/assets/images/emoji/close.svg b/frontend/app_flowy/assets/images/emoji/close.svg new file mode 100644 index 0000000000..822d63d82d --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/close.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/favorite_active.svg b/frontend/app_flowy/assets/images/emoji/favorite_active.svg new file mode 100644 index 0000000000..859822c00e --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/favorite_active.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg b/frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg new file mode 100644 index 0000000000..f56e138a31 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/image.svg b/frontend/app_flowy/assets/images/emoji/image.svg new file mode 100644 index 0000000000..bf4fdffa85 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/image.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/page.svg b/frontend/app_flowy/assets/images/emoji/page.svg new file mode 100644 index 0000000000..1b7405cbc1 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/page.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index f8f094d1b0..7534e99d14 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -2,7 +2,11 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:flutter/material.dart'; import 'package:app_flowy/workspace/application/user/settings_user_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import 'dart:convert'; class SettingsUserView extends StatelessWidget { final UserProfilePB user; @@ -16,7 +20,7 @@ class SettingsUserView extends StatelessWidget { builder: (context, state) => SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [_renderUserNameInput(context)], + children: [_renderUserNameInput(context), const VSpace(20), const _CurrentIcon()], ), ), ), @@ -48,3 +52,111 @@ class _UserNameInput extends StatelessWidget { }); } } + +class _CurrentIcon extends StatefulWidget { + const _CurrentIcon({Key? key}) : super(key: key); + + @override + State<_CurrentIcon> createState() => _CurrentIconState(); +} + +class _CurrentIconState extends State<_CurrentIcon> { + String iconUrl = 'assets/images/emoji/page.svg'; + + _setIcon(String path) { + setState(() { + iconUrl = path; + }); + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select an Icon'), + children: [SizedBox(height: 300, width: 300, child: IconGallery(_setIcon))]); + }, + ); + }, + child: Column(children: [ + const Align(alignment: Alignment.topLeft, child: Text("Icon")), + Align( + alignment: Alignment.centerLeft, + child: SvgPicture.asset(iconUrl), + ), + ])), + ); + } +} + +class IconGallery extends StatelessWidget { + final Function setIcon; + const IconGallery(this.setIcon, {Key? key}) : super(key: key); + + Future> _initImages(BuildContext context) async { + // >> To get paths you need these 2 lines + final manifestContent = await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); + + final Map manifestMap = json.decode(manifestContent); + // >> To get paths you need these 2 lines + + final imagePaths = + manifestMap.keys.where((String key) => key.startsWith('assets/images/emoji/') && key.endsWith('.svg')).toList(); + + return imagePaths; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _initImages(context), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return GridView.count( + primary: false, + padding: const EdgeInsets.all(20), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + crossAxisCount: 5, + children: (snapshot.data ?? []).map((String fileName) { + return IconOption(fileName, 50.0, setIcon); + }).toList(), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } +} + +class IconOption extends StatelessWidget { + final String fileName; + final double size; + final Function setIcon; + + IconOption(this.fileName, this.size, this.setIcon, {Key? key}) : super(key: ValueKey(fileName)); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: GestureDetector( + onTap: () { + debugPrint('$fileName is tapped'); + setIcon(fileName); + }, + child: SvgPicture.asset(fileName), + ), + ); + } +} diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 5f4ff9c9b8..a32382fcaa 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -118,6 +118,7 @@ flutter: - assets/images/home/ - assets/images/editor/ - assets/images/grid/ + - assets/images/emoji/ - assets/images/grid/field/ - assets/images/grid/setting/ - assets/translations/ From 634170a86e10fb19f31ee2330ca20a5cf96801f8 Mon Sep 17 00:00:00 2001 From: Ian Su Date: Mon, 1 Aug 2022 23:12:34 +0800 Subject: [PATCH 002/224] feat: modify user setting layout --- .../settings/widgets/settings_user_view.dart | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 7534e99d14..cefbfae5e0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -4,7 +4,7 @@ import 'package:app_flowy/workspace/application/user/settings_user_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flowy_infra/image.dart'; import 'dart:convert'; @@ -61,11 +61,11 @@ class _CurrentIcon extends StatefulWidget { } class _CurrentIconState extends State<_CurrentIcon> { - String iconUrl = 'assets/images/emoji/page.svg'; + String iconName = 'page'; - _setIcon(String path) { + _setIcon(String name) { setState(() { - iconUrl = path; + iconName = name; }); Navigator.of(context).pop(); } @@ -86,11 +86,19 @@ class _CurrentIconState extends State<_CurrentIcon> { ); }, child: Column(children: [ - const Align(alignment: Alignment.topLeft, child: Text("Icon")), + const Align( + alignment: Alignment.topLeft, + child: Text( + "Icon", + style: TextStyle(color: Colors.grey), + )), Align( - alignment: Alignment.centerLeft, - child: SvgPicture.asset(iconUrl), - ), + alignment: Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.all(5.0), + decoration: BoxDecoration(border: Border.all(color: Colors.grey)), + child: svgWithSize('emoji/$iconName', const Size(60, 60)), + )), ])), ); } @@ -100,23 +108,26 @@ class IconGallery extends StatelessWidget { final Function setIcon; const IconGallery(this.setIcon, {Key? key}) : super(key: key); - Future> _initImages(BuildContext context) async { + Future> _getIconNames(BuildContext context) async { // >> To get paths you need these 2 lines final manifestContent = await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); final Map manifestMap = json.decode(manifestContent); // >> To get paths you need these 2 lines - final imagePaths = - manifestMap.keys.where((String key) => key.startsWith('assets/images/emoji/') && key.endsWith('.svg')).toList(); + final iconNames = manifestMap.keys + .where((String key) => key.startsWith('assets/images/emoji/') && key.endsWith('.svg')) + .map((String key) => key.split('/').last.split('.').first) + .toList(); - return imagePaths; + debugPrint(iconNames.toString()); + return iconNames; } @override Widget build(BuildContext context) { return FutureBuilder>( - future: _initImages(context), + future: _getIconNames(context), builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasData) { return GridView.count( @@ -125,8 +136,8 @@ class IconGallery extends StatelessWidget { crossAxisSpacing: 10, mainAxisSpacing: 10, crossAxisCount: 5, - children: (snapshot.data ?? []).map((String fileName) { - return IconOption(fileName, 50.0, setIcon); + children: (snapshot.data ?? []).map((String iconName) { + return IconOption(iconName, 50.0, setIcon); }).toList(), ); } else { @@ -140,11 +151,11 @@ class IconGallery extends StatelessWidget { } class IconOption extends StatelessWidget { - final String fileName; + final String iconName; final double size; final Function setIcon; - IconOption(this.fileName, this.size, this.setIcon, {Key? key}) : super(key: ValueKey(fileName)); + IconOption(this.iconName, this.size, this.setIcon, {Key? key}) : super(key: ValueKey(iconName)); @override Widget build(BuildContext context) { @@ -152,10 +163,9 @@ class IconOption extends StatelessWidget { color: Colors.transparent, child: GestureDetector( onTap: () { - debugPrint('$fileName is tapped'); - setIcon(fileName); + setIcon(iconName); }, - child: SvgPicture.asset(fileName), + child: svgWidget('emoji/$iconName'), ), ); } From 7fc9a085c5adf589670e7ba62739fea4dbbff534 Mon Sep 17 00:00:00 2001 From: Ian Su Date: Wed, 3 Aug 2022 00:28:21 +0800 Subject: [PATCH 003/224] refactor: move icon state to SettingsUserViewBloc --- .../application/user/settings_user_bloc.dart | 6 ++ .../settings/widgets/settings_user_view.dart | 60 ++++++++----------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart index 7435778471..527eb7ba35 100644 --- a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -35,6 +35,9 @@ class SettingsUserViewBloc extends Bloc { ); }); }, + updateUserIcon: (String icon) { + emit(state.copyWith(icon: icon)); + }, ); }); } @@ -62,6 +65,7 @@ class SettingsUserViewBloc extends Bloc { class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.updateUserIcon(String icon) = _UpdateUserIcon; const factory SettingsUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; } @@ -70,10 +74,12 @@ class SettingsUserState with _$SettingsUserState { const factory SettingsUserState({ required UserProfilePB userProfile, required Either successOrFailure, + required String icon, }) = _SettingsUserState; factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( userProfile: userProfile, successOrFailure: left(unit), + icon: 'page', ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index cefbfae5e0..a6b67f143a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -20,7 +20,7 @@ class SettingsUserView extends StatelessWidget { builder: (context, state) => SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [_renderUserNameInput(context), const VSpace(20), const _CurrentIcon()], + children: [_renderUserNameInput(context), const VSpace(20), _renderCurrentIcon(context)], ), ), ), @@ -31,6 +31,11 @@ class SettingsUserView extends StatelessWidget { String name = context.read().state.userProfile.name; return _UserNameInput(name); } + + Widget _renderCurrentIcon(BuildContext context) { + String icon = context.read().state.icon; + return _CurrentIcon(icon); + } } class _UserNameInput extends StatelessWidget { @@ -53,25 +58,17 @@ class _UserNameInput extends StatelessWidget { } } -class _CurrentIcon extends StatefulWidget { - const _CurrentIcon({Key? key}) : super(key: key); - - @override - State<_CurrentIcon> createState() => _CurrentIconState(); -} - -class _CurrentIconState extends State<_CurrentIcon> { - String iconName = 'page'; - - _setIcon(String name) { - setState(() { - iconName = name; - }); - Navigator.of(context).pop(); - } +class _CurrentIcon extends StatelessWidget { + final String icon; + const _CurrentIcon(this.icon, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { + _setIcon(String icon) { + context.read().add(SettingsUserEvent.updateUserIcon(icon)); + Navigator.of(context).pop(); + } + return Material( color: Colors.transparent, child: GestureDetector( @@ -97,7 +94,7 @@ class _CurrentIconState extends State<_CurrentIcon> { child: Container( margin: const EdgeInsets.all(5.0), decoration: BoxDecoration(border: Border.all(color: Colors.grey)), - child: svgWithSize('emoji/$iconName', const Size(60, 60)), + child: svgWithSize('emoji/$icon', const Size(60, 60)), )), ])), ); @@ -108,36 +105,30 @@ class IconGallery extends StatelessWidget { final Function setIcon; const IconGallery(this.setIcon, {Key? key}) : super(key: key); - Future> _getIconNames(BuildContext context) async { - // >> To get paths you need these 2 lines + Future> _getIcons(BuildContext context) async { final manifestContent = await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); final Map manifestMap = json.decode(manifestContent); - // >> To get paths you need these 2 lines - final iconNames = manifestMap.keys + final icons = manifestMap.keys .where((String key) => key.startsWith('assets/images/emoji/') && key.endsWith('.svg')) .map((String key) => key.split('/').last.split('.').first) .toList(); - debugPrint(iconNames.toString()); - return iconNames; + return icons; } @override Widget build(BuildContext context) { return FutureBuilder>( - future: _getIconNames(context), + future: _getIcons(context), builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasData) { return GridView.count( - primary: false, padding: const EdgeInsets.all(20), - crossAxisSpacing: 10, - mainAxisSpacing: 10, crossAxisCount: 5, - children: (snapshot.data ?? []).map((String iconName) { - return IconOption(iconName, 50.0, setIcon); + children: (snapshot.data ?? []).map((String icon) { + return IconOption(icon, setIcon); }).toList(), ); } else { @@ -151,11 +142,10 @@ class IconGallery extends StatelessWidget { } class IconOption extends StatelessWidget { - final String iconName; - final double size; + final String icon; final Function setIcon; - IconOption(this.iconName, this.size, this.setIcon, {Key? key}) : super(key: ValueKey(iconName)); + IconOption(this.icon, this.setIcon, {Key? key}) : super(key: ValueKey(icon)); @override Widget build(BuildContext context) { @@ -163,9 +153,9 @@ class IconOption extends StatelessWidget { color: Colors.transparent, child: GestureDetector( onTap: () { - setIcon(iconName); + setIcon(icon); }, - child: svgWidget('emoji/$iconName'), + child: svgWidget('emoji/$icon'), ), ); } From 4eccdf3d286d5029a31e21a2ccdc3ea3234fdf61 Mon Sep 17 00:00:00 2001 From: Ian Su Date: Sat, 6 Aug 2022 22:31:55 +0800 Subject: [PATCH 004/224] feat: save icon into db --- .../lib/user/application/user_service.dart | 5 ++++ .../application/user/settings_user_bloc.dart | 9 ++++-- .../settings/widgets/settings_user_view.dart | 2 +- .../rust-lib/flowy-database/src/schema.rs | 1 + .../flowy-user/src/entities/parser/mod.rs | 2 ++ .../src/entities/parser/user_icon.rs | 16 +++++++++++ .../flowy-user/src/entities/user_profile.rs | 28 ++++++++++++++++++- .../flowy-user/src/services/database.rs | 5 ++++ 8 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs diff --git a/frontend/app_flowy/lib/user/application/user_service.dart b/frontend/app_flowy/lib/user/application/user_service.dart index 48bea6aa41..28b904fb5c 100644 --- a/frontend/app_flowy/lib/user/application/user_service.dart +++ b/frontend/app_flowy/lib/user/application/user_service.dart @@ -19,6 +19,7 @@ class UserService { String? name, String? password, String? email, + String? icon, }) { var payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -34,6 +35,10 @@ class UserService { payload.email = email; } + if (icon != null) { + payload.icon = icon; + } + return UserEventUpdateUserProfile(payload).send(); } diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart index 527eb7ba35..18c3a76fb4 100644 --- a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -36,7 +36,12 @@ class SettingsUserViewBloc extends Bloc { }); }, updateUserIcon: (String icon) { - emit(state.copyWith(icon: icon)); + _userService.updateUserProfile(icon: icon).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); }, ); }); @@ -74,12 +79,10 @@ class SettingsUserState with _$SettingsUserState { const factory SettingsUserState({ required UserProfilePB userProfile, required Either successOrFailure, - required String icon, }) = _SettingsUserState; factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( userProfile: userProfile, successOrFailure: left(unit), - icon: 'page', ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index a6b67f143a..9e7b2904cb 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -33,7 +33,7 @@ class SettingsUserView extends StatelessWidget { } Widget _renderCurrentIcon(BuildContext context) { - String icon = context.read().state.icon; + String icon = context.read().state.userProfile.icon; return _CurrentIcon(icon); } } diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index e41fd6d865..4a68c709dd 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -87,6 +87,7 @@ table! { name -> Text, token -> Text, email -> Text, + icon -> Text, workspace -> Text, } } diff --git a/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs b/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs index 71259509f2..792af0c146 100644 --- a/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs +++ b/frontend/rust-lib/flowy-user/src/entities/parser/mod.rs @@ -1,11 +1,13 @@ // https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ mod user_email; +mod user_icon; mod user_id; mod user_name; mod user_password; mod user_workspace; pub use user_email::*; +pub use user_icon::*; pub use user_id::*; pub use user_name::*; pub use user_password::*; diff --git a/frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs b/frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs new file mode 100644 index 0000000000..69258ca848 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/entities/parser/user_icon.rs @@ -0,0 +1,16 @@ +use crate::errors::ErrorCode; + +#[derive(Debug)] +pub struct UserIcon(pub String); + +impl UserIcon { + pub fn parse(s: String) -> Result { + Ok(Self(s)) + } +} + +impl AsRef for UserIcon { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 276894ffc8..d19f3293c8 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -2,7 +2,7 @@ use flowy_derive::ProtoBuf; use std::convert::TryInto; use crate::{ - entities::parser::{UserEmail, UserId, UserName, UserPassword}, + entities::parser::{UserEmail, UserIcon, UserId, UserName, UserPassword}, errors::ErrorCode, }; @@ -25,6 +25,9 @@ pub struct UserProfilePB { #[pb(index = 4)] pub token: String, + + #[pb(index = 5)] + pub icon: String, } #[derive(ProtoBuf, Default)] @@ -40,6 +43,9 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 4, one_of)] pub password: Option, + + #[pb(index = 5, one_of)] + pub icon: Option, } impl UpdateUserProfilePayloadPB { @@ -64,6 +70,11 @@ impl UpdateUserProfilePayloadPB { self.password = Some(password.to_owned()); self } + + pub fn icon(mut self, icon: &str) -> Self { + self.icon = Some(icon.to_owned()); + self + } } #[derive(ProtoBuf, Default, Clone, Debug)] @@ -79,6 +90,9 @@ pub struct UpdateUserProfileParams { #[pb(index = 4, one_of)] pub password: Option, + + #[pb(index = 5, one_of)] + pub icon: Option, } impl UpdateUserProfileParams { @@ -88,6 +102,7 @@ impl UpdateUserProfileParams { name: None, email: None, password: None, + icon: None, } } @@ -105,6 +120,11 @@ impl UpdateUserProfileParams { self.password = Some(password.to_owned()); self } + + pub fn icon(mut self, icon: &str) -> Self { + self.icon = Some(icon.to_owned()); + self + } } impl TryInto for UpdateUserProfilePayloadPB { @@ -128,11 +148,17 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(password) => Some(UserPassword::parse(password)?.0), }; + let icon = match self.icon { + None => None, + Some(icon) => Some(UserIcon::parse(icon)?.0), + }; + Ok(UpdateUserProfileParams { id, name, email, password, + icon, }) } } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index 64ebd705a7..7e214adb68 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -81,6 +81,7 @@ pub struct UserTable { pub(crate) name: String, pub(crate) token: String, pub(crate) email: String, + pub(crate) icon: String, pub(crate) workspace: String, // deprecated } @@ -91,6 +92,7 @@ impl UserTable { name, email, token, + icon: "".to_owned(), workspace: "".to_owned(), } } @@ -120,6 +122,7 @@ impl std::convert::From for UserProfilePB { email: table.email, name: table.name, token: table.token, + icon: table.icon, } } } @@ -131,6 +134,7 @@ pub struct UserTableChangeset { pub workspace: Option, // deprecated pub name: Option, pub email: Option, + pub icon: Option, } impl UserTableChangeset { @@ -140,6 +144,7 @@ impl UserTableChangeset { workspace: None, name: params.name, email: params.email, + icon: params.icon, } } } From db193376090713d77abd089376934dcfbc893296 Mon Sep 17 00:00:00 2001 From: Ian Su Date: Sun, 7 Aug 2022 00:05:12 +0800 Subject: [PATCH 005/224] feat: show icon in avatar --- .../presentation/home/menu/menu_user.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index d7b8dce4af..defd8cce15 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -19,7 +19,8 @@ class MenuUser extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => getIt(param1: user)..add(const MenuUserEvent.initial()), + create: (context) => + getIt(param1: user)..add(const MenuUserEvent.initial()), child: BlocBuilder( builder: (context, state) => Row( children: [ @@ -39,20 +40,16 @@ class MenuUser extends StatelessWidget { } Widget _renderAvatar(BuildContext context) { - return const SizedBox( + String icon = context.read().state.userProfile.icon; + + return SizedBox( width: 25, height: 25, child: ClipRRect( borderRadius: Corners.s5Border, child: CircleAvatar( - backgroundColor: Color.fromRGBO(132, 39, 224, 1.0), - child: Text( - 'M', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w300, - ), - ), + backgroundColor: Colors.transparent, + child: svgWidget('emoji/$icon'), )), ); } From d56e8c7673c2904e4ba6c91b6efed328fa325c8c Mon Sep 17 00:00:00 2001 From: Ian Su Date: Mon, 8 Aug 2022 22:19:05 +0800 Subject: [PATCH 006/224] fix: add icon_url in migration --- .../lib/user/application/user_service.dart | 15 +++-- .../application/user/settings_user_bloc.dart | 16 +++-- .../presentation/home/menu/menu_user.dart | 4 +- .../settings/widgets/settings_user_view.dart | 61 ++++++++++++------- .../2022-08-08-110959_user-add-icon/down.sql | 1 + .../2022-08-08-110959_user-add-icon/up.sql | 1 + .../rust-lib/flowy-database/src/schema.rs | 2 +- .../flowy-user/src/entities/user_profile.rs | 22 +++---- .../flowy-user/src/services/database.rs | 10 +-- 9 files changed, 79 insertions(+), 53 deletions(-) create mode 100644 frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql create mode 100644 frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql diff --git a/frontend/app_flowy/lib/user/application/user_service.dart b/frontend/app_flowy/lib/user/application/user_service.dart index 28b904fb5c..35e32f2eb1 100644 --- a/frontend/app_flowy/lib/user/application/user_service.dart +++ b/frontend/app_flowy/lib/user/application/user_service.dart @@ -11,7 +11,8 @@ class UserService { UserService({ required this.userId, }); - Future> getUserProfile({required String userId}) { + Future> getUserProfile( + {required String userId}) { return UserEventGetUserProfile().send(); } @@ -19,7 +20,7 @@ class UserService { String? name, String? password, String? email, - String? icon, + String? iconUrl, }) { var payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -35,14 +36,15 @@ class UserService { payload.email = email; } - if (icon != null) { - payload.icon = icon; + if (iconUrl != null) { + payload.iconUrl = iconUrl; } return UserEventUpdateUserProfile(payload).send(); } - Future> deleteWorkspace({required String workspaceId}) { + Future> deleteWorkspace( + {required String workspaceId}) { throw UnimplementedError(); } @@ -75,7 +77,8 @@ class UserService { }); } - Future> createWorkspace(String name, String desc) { + Future> createWorkspace( + String name, String desc) { final request = CreateWorkspacePayloadPB.create() ..name = name ..desc = desc; diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart index 18c3a76fb4..de63777812 100644 --- a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -35,8 +35,8 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - updateUserIcon: (String icon) { - _userService.updateUserProfile(icon: icon).then((result) { + updateUserIcon: (String iconUrl) { + _userService.updateUserProfile(iconUrl: iconUrl).then((result) { result.fold( (l) => null, (err) => Log.error(err), @@ -60,7 +60,8 @@ class SettingsUserViewBloc extends Bloc { void _profileUpdated(Either userProfileOrFailed) { userProfileOrFailed.fold( - (newUserProfile) => add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (newUserProfile) => + add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), (err) => Log.error(err), ); } @@ -70,8 +71,10 @@ class SettingsUserViewBloc extends Bloc { class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; - const factory SettingsUserEvent.updateUserIcon(String icon) = _UpdateUserIcon; - const factory SettingsUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; + const factory SettingsUserEvent.updateUserIcon(String iconUrl) = + _UpdateUserIcon; + const factory SettingsUserEvent.didReceiveUserProfile( + UserProfilePB newUserProfile) = _DidReceiveUserProfile; } @freezed @@ -81,7 +84,8 @@ class SettingsUserState with _$SettingsUserState { required Either successOrFailure, }) = _SettingsUserState; - factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( + factory SettingsUserState.initial(UserProfilePB userProfile) => + SettingsUserState( userProfile: userProfile, successOrFailure: left(unit), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index defd8cce15..baf9dbffab 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -40,7 +40,7 @@ class MenuUser extends StatelessWidget { } Widget _renderAvatar(BuildContext context) { - String icon = context.read().state.userProfile.icon; + String iconUrl = context.read().state.userProfile.iconUrl; return SizedBox( width: 25, @@ -49,7 +49,7 @@ class MenuUser extends StatelessWidget { borderRadius: Corners.s5Border, child: CircleAvatar( backgroundColor: Colors.transparent, - child: svgWidget('emoji/$icon'), + child: svgWidget('emoji/$iconUrl'), )), ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index 9e7b2904cb..a235fe7dcf 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -15,12 +15,17 @@ class SettingsUserView extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => getIt(param1: user)..add(const SettingsUserEvent.initial()), + create: (context) => getIt(param1: user) + ..add(const SettingsUserEvent.initial()), child: BlocBuilder( builder: (context, state) => SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [_renderUserNameInput(context), const VSpace(20), _renderCurrentIcon(context)], + children: [ + _renderUserNameInput(context), + const VSpace(20), + _renderCurrentIcon(context) + ], ), ), ), @@ -33,8 +38,9 @@ class SettingsUserView extends StatelessWidget { } Widget _renderCurrentIcon(BuildContext context) { - String icon = context.read().state.userProfile.icon; - return _CurrentIcon(icon); + String iconUrl = + context.read().state.userProfile.iconUrl; + return _CurrentIcon(iconUrl); } } @@ -53,19 +59,23 @@ class _UserNameInput extends StatelessWidget { labelText: 'Name', ), onSubmitted: (val) { - context.read().add(SettingsUserEvent.updateUserName(val)); + context + .read() + .add(SettingsUserEvent.updateUserName(val)); }); } } class _CurrentIcon extends StatelessWidget { - final String icon; - const _CurrentIcon(this.icon, {Key? key}) : super(key: key); + final String iconUrl; + const _CurrentIcon(this.iconUrl, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - _setIcon(String icon) { - context.read().add(SettingsUserEvent.updateUserIcon(icon)); + _setIcon(String iconUrl) { + context + .read() + .add(SettingsUserEvent.updateUserIcon(iconUrl)); Navigator.of(context).pop(); } @@ -78,7 +88,10 @@ class _CurrentIcon extends StatelessWidget { builder: (BuildContext context) { return SimpleDialog( title: const Text('Select an Icon'), - children: [SizedBox(height: 300, width: 300, child: IconGallery(_setIcon))]); + children: [ + SizedBox( + height: 300, width: 300, child: IconGallery(_setIcon)) + ]); }, ); }, @@ -93,8 +106,9 @@ class _CurrentIcon extends StatelessWidget { alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.all(5.0), - decoration: BoxDecoration(border: Border.all(color: Colors.grey)), - child: svgWithSize('emoji/$icon', const Size(60, 60)), + decoration: + BoxDecoration(border: Border.all(color: Colors.grey)), + child: svgWithSize('emoji/$iconUrl', const Size(60, 60)), )), ])), ); @@ -106,16 +120,18 @@ class IconGallery extends StatelessWidget { const IconGallery(this.setIcon, {Key? key}) : super(key: key); Future> _getIcons(BuildContext context) async { - final manifestContent = await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); + final manifestContent = + await DefaultAssetBundle.of(context).loadString('AssetManifest.json'); final Map manifestMap = json.decode(manifestContent); - final icons = manifestMap.keys - .where((String key) => key.startsWith('assets/images/emoji/') && key.endsWith('.svg')) + final iconUrls = manifestMap.keys + .where((String key) => + key.startsWith('assets/images/emoji/') && key.endsWith('.svg')) .map((String key) => key.split('/').last.split('.').first) .toList(); - return icons; + return iconUrls; } @override @@ -127,8 +143,8 @@ class IconGallery extends StatelessWidget { return GridView.count( padding: const EdgeInsets.all(20), crossAxisCount: 5, - children: (snapshot.data ?? []).map((String icon) { - return IconOption(icon, setIcon); + children: (snapshot.data ?? []).map((String iconUrl) { + return IconOption(iconUrl, setIcon); }).toList(), ); } else { @@ -142,10 +158,11 @@ class IconGallery extends StatelessWidget { } class IconOption extends StatelessWidget { - final String icon; + final String iconUrl; final Function setIcon; - IconOption(this.icon, this.setIcon, {Key? key}) : super(key: ValueKey(icon)); + IconOption(this.iconUrl, this.setIcon, {Key? key}) + : super(key: ValueKey(iconUrl)); @override Widget build(BuildContext context) { @@ -153,9 +170,9 @@ class IconOption extends StatelessWidget { color: Colors.transparent, child: GestureDetector( onTap: () { - setIcon(icon); + setIcon(iconUrl); }, - child: svgWidget('emoji/$icon'), + child: svgWidget('emoji/$iconUrl'), ), ); } diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql new file mode 100644 index 0000000000..505fbd4b2f --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/down.sql @@ -0,0 +1 @@ +ALTER TABLE user_table DROP COLUMN icon_url; diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql new file mode 100644 index 0000000000..c2aee5e3de --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-08-110959_user-add-icon/up.sql @@ -0,0 +1 @@ +ALTER TABLE user_table ADD COLUMN icon_url TEXT NOT NULL DEFAULT ''; diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index 4a68c709dd..eda9cd888b 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -87,8 +87,8 @@ table! { name -> Text, token -> Text, email -> Text, - icon -> Text, workspace -> Text, + icon_url -> Text, } } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index d19f3293c8..4b423db3af 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -27,7 +27,7 @@ pub struct UserProfilePB { pub token: String, #[pb(index = 5)] - pub icon: String, + pub icon_url: String, } #[derive(ProtoBuf, Default)] @@ -45,7 +45,7 @@ pub struct UpdateUserProfilePayloadPB { pub password: Option, #[pb(index = 5, one_of)] - pub icon: Option, + pub icon_url: Option, } impl UpdateUserProfilePayloadPB { @@ -71,8 +71,8 @@ impl UpdateUserProfilePayloadPB { self } - pub fn icon(mut self, icon: &str) -> Self { - self.icon = Some(icon.to_owned()); + pub fn icon_url(mut self, icon_url: &str) -> Self { + self.icon_url = Some(icon_url.to_owned()); self } } @@ -92,7 +92,7 @@ pub struct UpdateUserProfileParams { pub password: Option, #[pb(index = 5, one_of)] - pub icon: Option, + pub icon_url: Option, } impl UpdateUserProfileParams { @@ -102,7 +102,7 @@ impl UpdateUserProfileParams { name: None, email: None, password: None, - icon: None, + icon_url: None, } } @@ -121,8 +121,8 @@ impl UpdateUserProfileParams { self } - pub fn icon(mut self, icon: &str) -> Self { - self.icon = Some(icon.to_owned()); + pub fn icon_url(mut self, icon_url: &str) -> Self { + self.icon_url = Some(icon_url.to_owned()); self } } @@ -148,9 +148,9 @@ impl TryInto for UpdateUserProfilePayloadPB { Some(password) => Some(UserPassword::parse(password)?.0), }; - let icon = match self.icon { + let icon_url = match self.icon_url { None => None, - Some(icon) => Some(UserIcon::parse(icon)?.0), + Some(icon_url) => Some(UserIcon::parse(icon_url)?.0), }; Ok(UpdateUserProfileParams { @@ -158,7 +158,7 @@ impl TryInto for UpdateUserProfilePayloadPB { name, email, password, - icon, + icon_url, }) } } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index 7e214adb68..62b74c9f22 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -81,8 +81,8 @@ pub struct UserTable { pub(crate) name: String, pub(crate) token: String, pub(crate) email: String, - pub(crate) icon: String, pub(crate) workspace: String, // deprecated + pub(crate) icon_url: String, } impl UserTable { @@ -92,7 +92,7 @@ impl UserTable { name, email, token, - icon: "".to_owned(), + icon_url: "".to_owned(), workspace: "".to_owned(), } } @@ -122,7 +122,7 @@ impl std::convert::From for UserProfilePB { email: table.email, name: table.name, token: table.token, - icon: table.icon, + icon_url: table.icon_url, } } } @@ -134,7 +134,7 @@ pub struct UserTableChangeset { pub workspace: Option, // deprecated pub name: Option, pub email: Option, - pub icon: Option, + pub icon_url: Option, } impl UserTableChangeset { @@ -144,7 +144,7 @@ impl UserTableChangeset { workspace: None, name: params.name, email: params.email, - icon: params.icon, + icon_url: params.icon_url, } } } From 1cca7acf1b7ea304aac609c172c0d68b11db8e7c Mon Sep 17 00:00:00 2001 From: Aryman Date: Tue, 9 Aug 2022 01:43:04 +0530 Subject: [PATCH 007/224] perf: added right click context menu functionality --- .../menu/app/section/disclosure_action.dart | 64 ++++++++++++++++--- .../home/menu/app/section/item.dart | 45 +++++++++---- 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart index c9a02a9de7..83b652eb8b 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart @@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart' as dartz; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/theme.dart'; import 'package:provider/provider.dart'; @@ -11,10 +12,13 @@ import 'item.dart'; // [[Widget: LifeCycle]] // https://flutterbyexample.com/lesson/stateful-widget-lifecycle -class ViewDisclosureButton extends StatelessWidget with ActionList, FlowyOverlayDelegate { +class ViewDisclosureButton extends StatelessWidget + with ActionList, FlowyOverlayDelegate { final Function() onTap; final Function(dartz.Option) onSelected; - final _items = ViewDisclosureAction.values.map((action) => ViewDisclosureActionWrapper(action)).toList(); + final _items = ViewDisclosureAction.values + .map((action) => ViewDisclosureActionWrapper(action)) + .toList(); ViewDisclosureButton({ Key? key, @@ -40,12 +44,13 @@ class ViewDisclosureButton extends StatelessWidget with ActionList get items => _items; @override - void Function(dartz.Option p1) get selectCallback => (result) { - result.fold( - () => onSelected(dartz.none()), - (wrapper) => onSelected(dartz.some(wrapper.inner)), - ); - }; + void Function(dartz.Option p1) + get selectCallback => (result) { + result.fold( + () => onSelected(dartz.none()), + (wrapper) => onSelected(dartz.some(wrapper.inner)), + ); + }; @override FlowyOverlayDelegate? get delegate => this; @@ -56,6 +61,49 @@ class ViewDisclosureButton extends StatelessWidget with ActionList, FlowyOverlayDelegate { + final Widget child; + final Function(dartz.Option) onSelected; + final _items = ViewDisclosureAction.values + .map((action) => ViewDisclosureActionWrapper(action)) + .toList(); + + ViewDisclosureRegion( + {Key? key, required this.onSelected, required this.child}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: (event) => { + if (event.kind == PointerDeviceKind.mouse && + event.buttons == kSecondaryMouseButton) + { + print("IN LISTENER RN"), + show(context), + } + }, + child: child, + ); + } + + @override + FlowyOverlayDelegate? get delegate => this; + + @override + List get items => _items; + + @override + void Function(dartz.Option p1) + get selectCallback => (result) { + result.fold( + () => onSelected(dartz.none()), + (wrapper) => onSelected(dartz.some(wrapper.inner)), + ); + }; +} + class ViewDisclosureActionWrapper extends ActionItem { final ViewDisclosureAction inner; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index 97ae1dae52..93855c3fa2 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -36,7 +36,9 @@ class ViewSectionItem extends StatelessWidget { final theme = context.watch(); return MultiBlocProvider( providers: [ - BlocProvider(create: (ctx) => getIt(param1: view)..add(const ViewEvent.initial())), + BlocProvider( + create: (ctx) => + getIt(param1: view)..add(const ViewEvent.initial())), ], child: BlocBuilder( builder: (context, state) { @@ -46,7 +48,8 @@ class ViewSectionItem extends StatelessWidget { onTap: () => onSelected(context.read().state.view), child: FlowyHover( style: HoverStyle(hoverColor: theme.bg3), - builder: (_, onHover) => _render(context, onHover, state, theme.iconColor), + builder: (_, onHover) => + _render(context, onHover, state, theme.iconColor), setSelected: () => state.isEditing || isSelected, ), ), @@ -56,17 +59,24 @@ class ViewSectionItem extends StatelessWidget { ); } - Widget _render(BuildContext context, bool onHover, ViewState state, Color iconColor) { + Widget _render( + BuildContext context, bool onHover, ViewState state, Color iconColor) { List children = [ - SizedBox(width: 16, height: 16, child: state.view.renderThumbnail(iconColor: iconColor)), + SizedBox( + width: 16, + height: 16, + child: state.view.renderThumbnail(iconColor: iconColor)), const HSpace(2), - Expanded(child: FlowyText.regular(state.view.name, fontSize: 12, overflow: TextOverflow.clip)), + Expanded( + child: FlowyText.regular(state.view.name, + fontSize: 12, overflow: TextOverflow.clip)), ]; if (onHover || state.isEditing) { children.add( ViewDisclosureButton( - onTap: () => context.read().add(const ViewEvent.setIsEditing(true)), + onTap: () => + context.read().add(const ViewEvent.setIsEditing(true)), onSelected: (action) { context.read().add(const ViewEvent.setIsEditing(false)); _handleAction(context, action); @@ -75,16 +85,23 @@ class ViewSectionItem extends StatelessWidget { ); } - return SizedBox( - height: 26, - child: Row(children: children).padding( - left: MenuAppSizes.expandedPadding, - right: MenuAppSizes.headerPadding, - ), - ); + return ViewDisclosureRegion( + // context.read().add(const ViewEvent.setIsEditing(true)), + onSelected: (action) { + context.read().add(const ViewEvent.setIsEditing(false)); + _handleAction(context, action); + }, + child: SizedBox( + height: 26, + child: Row(children: children).padding( + left: MenuAppSizes.expandedPadding, + right: MenuAppSizes.headerPadding, + ), + )); } - void _handleAction(BuildContext context, dartz.Option action) { + void _handleAction( + BuildContext context, dartz.Option action) { action.foldRight({}, (action, previous) { switch (action) { case ViewDisclosureAction.rename: From c8d5769b1140c58e8ff736bd068b1fdd9ac8710e Mon Sep 17 00:00:00 2001 From: Aryman Date: Tue, 9 Aug 2022 01:45:56 +0530 Subject: [PATCH 008/224] refactor: remove print statement --- .../presentation/home/menu/app/section/disclosure_action.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart index 83b652eb8b..7fe02bab1a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart @@ -80,7 +80,6 @@ class ViewDisclosureRegion extends StatelessWidget if (event.kind == PointerDeviceKind.mouse && event.buttons == kSecondaryMouseButton) { - print("IN LISTENER RN"), show(context), } }, From 06d11a91d14b38fab363e95743e19ae29ba6acd0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 10:44:00 +0800 Subject: [PATCH 009/224] feat: lineThrough and underline can coexist --- .../lib/render/rich_text/rich_text_style.dart | 11 +++++++---- .../format_rich_text_style.dart | 5 ----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index cc4f6038ac..29677cdce9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -204,12 +204,15 @@ class RichTextStyle { // underline or strikethrough TextDecoration get textDecoration { + var decorations = [TextDecoration.none]; if (attributes.underline || attributes.href != null) { - return TextDecoration.underline; - } else if (attributes.strikethrough) { - return TextDecoration.lineThrough; + decorations.add(TextDecoration.underline); + // TextDecoration.underline; } - return TextDecoration.none; + if (attributes.strikethrough) { + decorations.add(TextDecoration.lineThrough); + } + return TextDecoration.combine(decorations); } // font diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 46c7d3278f..ce6d733d73 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -97,11 +97,6 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey) { Attributes attributes = { styleKey: value, }; - if (styleKey == StyleKey.underline && value) { - attributes[StyleKey.strikethrough] = null; - } else if (styleKey == StyleKey.strikethrough && value) { - attributes[StyleKey.underline] = null; - } return formatRichTextStyle(editorState, attributes); } From fdbecd7f100bb1a8009ee2085ee280243d3b5dd8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 10:46:51 +0800 Subject: [PATCH 010/224] fix: typo --- .../flowy_editor/lib/render/rich_text/rich_text_style.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 29677cdce9..daad186fc2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -146,7 +146,7 @@ extension DeltaAttributesExtensions on Attributes { return null; } - Color? get hightlightColor { + Color? get highlightColor { if (containsKey(StyleKey.highlightColor) && this[StyleKey.highlightColor] is String) { return Color( @@ -228,7 +228,7 @@ class RichTextStyle { } Color get backgroundColor { - return attributes.hightlightColor ?? Colors.transparent; + return attributes.highlightColor ?? Colors.transparent; } // font size From f4a31768cbd2f15bcec0f85765b78a15002b112b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 11:20:53 +0800 Subject: [PATCH 011/224] feat: support rendering text background color and text color --- .../flowy_editor/example/assets/example.json | 6 ++- .../lib/render/rich_text/rich_text_style.dart | 46 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index b6fc3467dc..fe74b22dad 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -37,7 +37,11 @@ "type": "text", "delta": [ { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + "insert": "At " + }, + { "insert": "AppFlowy", "attributes": { "code": true, "bold": true, "color": "0xFFED459C"} }, + { + "insert": ", we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." } ] }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index daad186fc2..c1545b4080 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -183,19 +183,30 @@ class RichTextStyle { return TextSpan( text: text, style: TextStyle( - fontWeight: fontWeight, - fontStyle: fontStyle, - fontSize: fontSize, - color: textColor, - backgroundColor: backgroundColor, - decoration: textDecoration, + fontWeight: _fontWeight, + fontStyle: _fontStyle, + fontSize: _fontSize, + color: _textColor, + decoration: _textDecoration, + background: _background, ), - recognizer: recognizer, + recognizer: _recognizer, ); } + Paint? get _background { + if (_backgroundColor != null) { + return Paint() + ..color = _backgroundColor! + ..strokeWidth = 24.0 + ..style = PaintingStyle.fill + ..strokeJoin = StrokeJoin.round; + } + return null; + } + // bold - FontWeight get fontWeight { + FontWeight get _fontWeight { if (attributes.bold) { return FontWeight.bold; } @@ -203,7 +214,7 @@ class RichTextStyle { } // underline or strikethrough - TextDecoration get textDecoration { + TextDecoration get _textDecoration { var decorations = [TextDecoration.none]; if (attributes.underline || attributes.href != null) { decorations.add(TextDecoration.underline); @@ -216,28 +227,33 @@ class RichTextStyle { } // font - FontStyle get fontStyle => + FontStyle get _fontStyle => attributes.italic ? FontStyle.italic : FontStyle.normal; // text color - Color get textColor { + Color get _textColor { if (attributes.href != null) { return Colors.lightBlue; } return attributes.color ?? Colors.black; } - Color get backgroundColor { - return attributes.highlightColor ?? Colors.transparent; + Color? get _backgroundColor { + if (attributes.highlightColor != null) { + return attributes.highlightColor!; + } else if (attributes.code) { + return Colors.grey.withOpacity(0.4); + } + return null; } // font size - double get fontSize { + double get _fontSize { return baseFontSize; } // recognizer - GestureRecognizer? get recognizer { + GestureRecognizer? get _recognizer { final href = attributes.href; if (href != null) { return TapGestureRecognizer() From b9c0c1209ac3114d78552d8bd03fd65c52079675 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 11:46:40 +0800 Subject: [PATCH 012/224] feat: support more command + x shortcut --- .../update_text_style_by_command_x_handler.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 220643cf6f..02073563eb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -23,6 +23,18 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { case 'b': formatBold(editorState); return KeyEventResult.handled; + case 'I': + case 'i': + formatItalic(editorState); + return KeyEventResult.handled; + case 'U': + case 'u': + formatUnderline(editorState); + return KeyEventResult.handled; + case 'S': + case 's': + formatStrikethrough(editorState); + return KeyEventResult.handled; default: break; } From 7c58654fe827cf0508b4e03555412753a99b6d85 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Tue, 9 Aug 2022 13:33:54 +0800 Subject: [PATCH 013/224] fix: undo redo handler --- .../flowy_editor/lib/service/editor_service.dart | 2 ++ .../redo_undo_handler.dart | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 78f7bb76fa..2d042377e1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -17,6 +17,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/enter_without_s import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/scroll_service.dart'; @@ -39,6 +40,7 @@ List defaultKeyEventHandler = [ flowyDeleteNodesHandler, arrowKeysHandler, copyPasteKeysHandler, + redoUndoKeysHandler, enterWithoutShiftInTextNodesHandler, updateTextStyleByCommandXHandler, whiteSpaceHandler, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart new file mode 100644 index 0000000000..75b22402e4 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart @@ -0,0 +1,15 @@ +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +FlowyKeyEventHandler redoUndoKeysHandler = (editorState, event) { + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyZ) { + if (event.isShiftPressed) { + editorState.undoManager.redo(); + } else { + editorState.undoManager.undo(); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; From 4464f2abfe9c786bc11ed2cac1e90bdf6a4adcc2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 14:11:34 +0800 Subject: [PATCH 014/224] feat: make the text node widget align center. --- .../lib/render/editor/editor_entry.dart | 2 +- .../render/rich_text/bulleted_list_text.dart | 22 +++--- .../lib/render/rich_text/checkbox_text.dart | 41 +++++------ .../lib/render/rich_text/flowy_rich_text.dart | 16 ----- .../lib/render/rich_text/heading_text.dart | 35 +++++----- .../render/rich_text/number_list_text.dart | 21 +++--- .../lib/render/rich_text/quoted_text.dart | 25 +++---- .../lib/render/rich_text/rich_text.dart | 68 +++++++++++++++++++ .../lib/render/rich_text/rich_text_style.dart | 2 + .../lib/service/editor_service.dart | 2 +- 10 files changed, 145 insertions(+), 89 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart index fa32743b02..9be82fa31a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart @@ -33,7 +33,7 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: node.children .map( (child) => diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart index 75cde60e39..05da8d1e80 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -3,6 +3,7 @@ import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/infra/flowy_svg.dart'; import 'package:flowy_editor/render/rich_text/default_selectable.dart'; import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; @@ -56,21 +57,22 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Row( - children: [ - FlowySvg( - size: Size.square(leftPadding), - name: 'point', - ), - Expanded( - child: FlowyRichText( + return SizedBox( + width: maxTextNodeWidth, + child: Row( + children: [ + FlowySvg( + size: Size.square(leftPadding), + name: 'point', + ), + FlowyRichText( key: _richTextKey, placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, ), - ), - ], + ], + ), ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 317d1a6bdf..89c314eb8a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -65,33 +65,34 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - child: FlowySvg( - size: Size.square(leftPadding), - name: check ? 'check' : 'uncheck', + return SizedBox( + width: maxTextNodeWidth, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: FlowySvg( + size: Size.square(leftPadding), + name: check ? 'check' : 'uncheck', + ), + onTap: () { + debugPrint('[Checkbox] onTap...'); + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + StyleKey.checkbox: !check, + }) + ..commit(); + }, ), - onTap: () { - debugPrint('[Checkbox] onTap...'); - TransactionBuilder(widget.editorState) - ..updateNode(widget.textNode, { - StyleKey.checkbox: !check, - }) - ..commit(); - }, - ), - Expanded( - child: FlowyRichText( + FlowyRichText( key: _richTextKey, placeholderText: 'To-do', textNode: widget.textNode, textSpanDecorator: _textSpanDecorator, editorState: widget.editorState, ), - ), - ], + ], + ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 6250c62db2..882f98c4d5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -11,22 +11,6 @@ import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; -class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { - @override - Widget build(NodeWidgetContext context) { - return FlowyRichText( - key: context.node.key, - textNode: context.node, - editorState: context.editorState, - ); - } - - @override - NodeValidator get nodeValidator => ((node) { - return true; - }); -} - typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); class FlowyRichText extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart index 2511349e4d..f74064fac6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -56,25 +56,22 @@ class _HeadingTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: EdgeInsets.only( - top: topPadding, - bottom: bottomPadding, - ), - child: Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Heading', - placeholderTextSpanDecorator: _placeholderTextSpanDecorator, - textSpanDecorator: _textSpanDecorator, - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ) - ], + return SizedBox( + width: maxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only( + top: topPadding, + bottom: bottomPadding, + ), + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Heading', + placeholderTextSpanDecorator: _placeholderTextSpanDecorator, + textSpanDecorator: _textSpanDecorator, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index e9fed70c54..0a2cd5937a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -57,21 +57,22 @@ class _NumberListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Row( - children: [ - FlowySvg( - size: Size.square(leftPadding), - number: widget.textNode.attributes.number, - ), - Expanded( - child: FlowyRichText( + return SizedBox( + width: maxTextNodeWidth, + child: Row( + children: [ + FlowySvg( + size: Size.square(leftPadding), + number: widget.textNode.attributes.number, + ), + FlowyRichText( key: _richTextKey, placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, ), - ), - ], + ], + ), ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart index 00bb393652..65cfa3d066 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -56,24 +56,25 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Row( - children: [ - FlowySvg( - size: Size( - leftPadding, - _quoteHeight, + return SizedBox( + width: maxTextNodeWidth, + child: Row( + children: [ + FlowySvg( + size: Size( + leftPadding, + _quoteHeight, + ), + name: 'quote', ), - name: 'quote', - ), - Expanded( - child: FlowyRichText( + FlowyRichText( key: _richTextKey, placeholderText: 'Quote', textNode: widget.textNode, editorState: widget.editorState, ), - ), - ], + ], + ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart new file mode 100644 index 0000000000..b6d79a2358 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart @@ -0,0 +1,68 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flutter/material.dart'; + +class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return RichTextNodeWidget( + key: context.node.key, + textNode: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => ((node) { + return true; + }); +} + +class RichTextNodeWidget extends StatefulWidget { + const RichTextNodeWidget({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State createState() => _RichTextNodeWidgetState(); +} + +// customize + +class _RichTextNodeWidgetState extends State + with Selectable, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'rich_text'); + final leftPadding = 20.0; + + @override + Selectable get forward => + _richTextKey.currentState as Selectable; + + @override + Offset get baseOffset { + return Offset.zero; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: maxTextNodeWidth, + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index c1545b4080..2fb12d68ac 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -59,6 +59,8 @@ class StyleKey { ]; } +// TODO: customize +double maxTextNodeWidth = 780.0; double baseFontSize = 16.0; // TODO: customize. Map headingToFontSize = { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 78f7bb76fa..0593706f89 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,10 +1,10 @@ +import 'package:flowy_editor/render/rich_text/rich_text.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/render/editor/editor_entry.dart'; import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; From 6f32f749bc1051ad43816d17a168f82fb10a8724 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 15:14:38 +0800 Subject: [PATCH 015/224] fix: delete style widget error --- .../packages/flowy_editor/lib/service/editor_service.dart | 2 +- .../enter_without_shift_in_text_node_handler.dart | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 0593706f89..38156dbb6b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,4 +1,3 @@ -import 'package:flowy_editor/render/rich_text/rich_text.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/editor_state.dart'; @@ -8,6 +7,7 @@ import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; import 'package:flowy_editor/render/rich_text/heading_text.dart'; import 'package:flowy_editor/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/render/rich_text/quoted_text.dart'; +import 'package:flowy_editor/render/rich_text/rich_text.dart'; import 'package:flowy_editor/service/input_service.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index cf71830386..d2d3b14450 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; @@ -91,7 +92,8 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = ..insertNode( textNode.path.next, textNode.copyWith( - attributes: needCopyAttributes ? textNode.attributes : {}, + attributes: + needCopyAttributes ? Attributes.from(textNode.attributes) : {}, delta: textNode.delta.slice(selection.end.offset), ), ) From 255cb47876e7b2cf0495bb01e27b3b6efed09ab1 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 15:32:28 +0800 Subject: [PATCH 016/224] fix: text align error --- .../lib/render/rich_text/bulleted_list_text.dart | 13 ++++++++----- .../lib/render/rich_text/checkbox_text.dart | 14 ++++++++------ .../lib/render/rich_text/number_list_text.dart | 13 ++++++++----- .../lib/render/rich_text/quoted_text.dart | 13 ++++++++----- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart index 05da8d1e80..b962f63f3d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -60,16 +60,19 @@ class _BulletedListTextNodeWidgetState extends State return SizedBox( width: maxTextNodeWidth, child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ FlowySvg( size: Size.square(leftPadding), name: 'point', ), - FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, + ), ), ], ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 89c314eb8a..e5b02eb32d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -84,12 +84,14 @@ class _CheckboxNodeWidgetState extends State ..commit(); }, ), - FlowyRichText( - key: _richTextKey, - placeholderText: 'To-do', - textNode: widget.textNode, - textSpanDecorator: _textSpanDecorator, - editorState: widget.editorState, + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'To-do', + textNode: widget.textNode, + textSpanDecorator: _textSpanDecorator, + editorState: widget.editorState, + ), ), ], ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index 0a2cd5937a..65b41e8e9b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -60,16 +60,19 @@ class _NumberListTextNodeWidgetState extends State return SizedBox( width: maxTextNodeWidth, child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ FlowySvg( size: Size.square(leftPadding), number: widget.textNode.attributes.number, ), - FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, + ), ), ], ), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart index 65cfa3d066..0bb259de14 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -59,6 +59,7 @@ class _QuotedTextNodeWidgetState extends State return SizedBox( width: maxTextNodeWidth, child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ FlowySvg( size: Size( @@ -67,11 +68,13 @@ class _QuotedTextNodeWidgetState extends State ), name: 'quote', ), - FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, + ), ), ], ), From e9d8dc9657b73d628550dbef1ce3c6a05bbde8df Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 17:50:32 +0800 Subject: [PATCH 017/224] feat: increase line spacing --- .../flowy_editor/lib/infra/flowy_svg.dart | 9 +++ .../render/rich_text/bulleted_list_text.dart | 51 +++++++------ .../lib/render/rich_text/checkbox_text.dart | 76 ++++++++++--------- .../render/rich_text/default_selectable.dart | 12 ++- .../lib/render/rich_text/heading_text.dart | 22 +++--- .../render/rich_text/number_list_text.dart | 55 ++++++++------ .../lib/render/rich_text/quoted_text.dart | 58 +++++++------- .../lib/render/rich_text/rich_text.dart | 22 +++--- .../lib/render/rich_text/rich_text_style.dart | 28 ++++++- 9 files changed, 198 insertions(+), 135 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart index d38fe2d16d..d40a198b1a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart @@ -8,15 +8,24 @@ class FlowySvg extends StatelessWidget { this.size = const Size(20, 20), this.color, this.number, + this.padding, }) : super(key: key); final String? name; final Size size; final Color? color; final int? number; + final EdgeInsets? padding; @override Widget build(BuildContext context) { + return Padding( + padding: padding ?? const EdgeInsets.all(0), + child: _buildSvg(), + ); + } + + Widget _buildSvg() { if (name != null) { return SizedBox.fromSize( size: size, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart index b962f63f3d..2607be26ed 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart @@ -43,38 +43,45 @@ class BulletedListTextNodeWidget extends StatefulWidget { class _BulletedListTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + final iconKey = GlobalKey(); + final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text'); - final leftPadding = 20.0; + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; + return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - size: Size.square(leftPadding), - name: 'point', - ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + size: Size.square(_iconSize), + padding: + EdgeInsets.only(top: topPadding, right: _iconRightPadding), + name: 'point', ), - ), - ], + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), + ], + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index e5b02eb32d..890f80fd54 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -41,19 +41,17 @@ class CheckboxNodeWidget extends StatefulWidget { class _CheckboxNodeWidgetState extends State with Selectable, DefaultSelectable { - final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); + @override + final iconKey = GlobalKey(); - final leftPadding = 20.0; + final _richTextKey = GlobalKey(debugLabel: 'checkbox_text'); + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { if (widget.textNode.children.isEmpty) { @@ -65,37 +63,43 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - child: FlowySvg( - size: Size.square(leftPadding), - name: check ? 'check' : 'uncheck', - ), - onTap: () { - debugPrint('[Checkbox] onTap...'); - TransactionBuilder(widget.editorState) - ..updateNode(widget.textNode, { - StyleKey.checkbox: !check, - }) - ..commit(); - }, + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: FlowySvg( + key: iconKey, + size: Size.square(_iconSize), + padding: EdgeInsets.only( + top: topPadding, right: _iconRightPadding), + name: check ? 'check' : 'uncheck', + ), + onTap: () { + debugPrint('[Checkbox] onTap...'); + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + StyleKey.checkbox: !check, + }) + ..commit(); + }, + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'To-do', + textNode: widget.textNode, + textSpanDecorator: _textSpanDecorator, + editorState: widget.editorState, + ), + ), + ], ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'To-do', - textNode: widget.textNode, - textSpanDecorator: _textSpanDecorator, - editorState: widget.editorState, - ), - ), - ], - ), - ); + )); } Widget _buildWithChildren(BuildContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart index 21cc5108f3..4fbe9c1521 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart @@ -6,7 +6,17 @@ import 'package:flutter/material.dart'; mixin DefaultSelectable { Selectable get forward; - Offset get baseOffset; + GlobalKey? get iconKey; + + Offset get baseOffset { + if (iconKey != null) { + final renderBox = iconKey!.currentContext?.findRenderObject(); + if (renderBox is RenderBox) { + return Offset(renderBox.size.width, 0); + } + } + return Offset.zero; + } Position getPositionInOffset(Offset start) => forward.getPositionInOffset(start); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart index f74064fac6..c010ad4833 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart @@ -41,9 +41,11 @@ class HeadingTextNodeWidget extends StatefulWidget { class _HeadingTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + GlobalKey? get iconKey => null; + final _richTextKey = GlobalKey(debugLabel: 'heading_text'); - final topPadding = 5.0; - final bottomPadding = 2.0; + final _topPadding = 5.0; @override Selectable get forward => @@ -51,18 +53,18 @@ class _HeadingTextNodeWidgetState extends State @override Offset get baseOffset { - return Offset(0, topPadding); + return Offset(0, _topPadding); } @override Widget build(BuildContext context) { - return SizedBox( - width: maxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only( - top: topPadding, - bottom: bottomPadding, - ), + return Padding( + padding: EdgeInsets.only( + top: _topPadding, + bottom: defaultLinePadding, + ), + child: SizedBox( + width: defaultMaxTextNodeWidth, child: FlowyRichText( key: _richTextKey, placeholderText: 'Heading', diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart index 65b41e8e9b..4ffd587470 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart @@ -43,39 +43,44 @@ class NumberListTextNodeWidget extends StatefulWidget { class _NumberListTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + final iconKey = GlobalKey(); + final _richTextKey = GlobalKey(debugLabel: 'number_list_text'); - final leftPadding = 20.0; + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { - return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - size: Size.square(leftPadding), - number: widget.textNode.attributes.number, + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: SizedBox( + width: defaultMaxTextNodeWidth, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + size: Size.square(_iconSize), + padding: + EdgeInsets.only(top: topPadding, right: _iconRightPadding), + number: widget.textNode.attributes.number, + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), + ], ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), - ); + )); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart index 0bb259de14..09004f7f9d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart @@ -42,48 +42,50 @@ class QuotedTextNodeWidget extends StatefulWidget { class _QuotedTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + final iconKey = GlobalKey(); + final _richTextKey = GlobalKey(debugLabel: 'quoted_text'); - final leftPadding = 20.0; + final _iconSize = 20.0; + final _iconRightPadding = 5.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset(leftPadding, 0); - } - @override Widget build(BuildContext context) { + final topPadding = RichTextStyle.fromTextNode(widget.textNode).topPadding; return SizedBox( - width: maxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - size: Size( - leftPadding, - _quoteHeight, - ), - name: 'quote', + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + size: Size(_iconSize, _quoteHeight), + padding: + EdgeInsets.only(top: topPadding, right: _iconRightPadding), + name: 'quote', + ), + Expanded( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, + ), + ), + ], ), - Expanded( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), - ); + )); } double get _quoteHeight { final lines = widget.textNode.toRawString().characters.where((c) => c == '\n').length; - return (lines + 1) * leftPadding; + return (lines + 1) * _iconSize; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart index b6d79a2358..bfb4c217a7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart @@ -42,26 +42,26 @@ class RichTextNodeWidget extends StatefulWidget { class _RichTextNodeWidgetState extends State with Selectable, DefaultSelectable { + @override + GlobalKey? get iconKey => null; + final _richTextKey = GlobalKey(debugLabel: 'rich_text'); - final leftPadding = 20.0; @override Selectable get forward => _richTextKey.currentState as Selectable; - @override - Offset get baseOffset { - return Offset.zero; - } - @override Widget build(BuildContext context) { return SizedBox( - width: maxTextNodeWidth, - child: FlowyRichText( - key: _richTextKey, - textNode: widget.textNode, - editorState: widget.editorState, + width: defaultMaxTextNodeWidth, + child: Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 2fb12d68ac..e39aed9aa2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -1,4 +1,5 @@ import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/document/node.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -60,7 +61,8 @@ class StyleKey { } // TODO: customize -double maxTextNodeWidth = 780.0; +double defaultMaxTextNodeWidth = 780.0; +double defaultLinePadding = 8.0; double baseFontSize = 16.0; // TODO: customize. Map headingToFontSize = { @@ -176,12 +178,33 @@ class RichTextStyle { RichTextStyle({ required this.attributes, required this.text, + this.height = 1.5, }); + RichTextStyle.fromTextNode(TextNode textNode) + : this(attributes: textNode.attributes, text: textNode.toRawString()); + final Attributes attributes; final String text; + final double height; - TextSpan toTextSpan() { + TextSpan toTextSpan() => _toTextSpan(height); + + double get topPadding { + if (height == 1.0) { + return 0; + } + // TODO: Need to be optimized. + final painter = + TextPainter(text: _toTextSpan(height), textDirection: TextDirection.ltr) + ..layout(); + final basePainter = + TextPainter(text: _toTextSpan(null), textDirection: TextDirection.ltr) + ..layout(); + return painter.height - basePainter.height; + } + + TextSpan _toTextSpan(double? height) { return TextSpan( text: text, style: TextStyle( @@ -191,6 +214,7 @@ class RichTextStyle { color: _textColor, decoration: _textDecoration, background: _background, + height: height, ), recognizer: _recognizer, ); From b7cb4b647ddbb48a8928b44b32d716c9b95a1a63 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 17:52:11 +0800 Subject: [PATCH 018/224] fix: the editor loses focus occasionally --- .../packages/flowy_editor/lib/service/selection_service.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index ecf8caf817..1786eca05f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -306,6 +306,9 @@ class _FlowySelectionState extends State return; } + editorState.service.keyboardService?.enable(); + editorState.service.scrollService?.enable(); + panEndOffset = details.globalPosition; final dy = editorState.service.scrollService?.dy; var panStartOffsetWithScrollDyGap = panStartOffset!; From 292b90c14cec3576e4624694ff08316a21075436 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 9 Aug 2022 18:04:23 +0800 Subject: [PATCH 019/224] fix: create column errors --- .../board/presentation/board_page.dart | 148 ++++++++++++++++- .../lib/plugins/doc/presentation/banner.dart | 18 +- .../cell_service/cell_field_notifier.dart | 7 +- .../cell/cell_service/context_builder.dart | 4 +- .../plugins/grid/application/grid_bloc.dart | 156 ++++++++---------- .../application/grid_data_controller.dart | 128 ++++++++++++++ .../grid/application/row/row_bloc.dart | 33 ++-- .../application/row/row_data_controller.dart | 41 +++++ .../grid/application/row/row_service.dart | 6 +- .../presentation/controller/grid_scroll.dart | 11 +- .../plugins/grid/presentation/grid_page.dart | 19 ++- .../widgets/cell/cell_builder.dart | 50 +++--- ...cell_cotainer.dart => cell_container.dart} | 0 .../widgets/cell/number_cell.dart | 6 +- .../select_option_cell.dart | 22 +-- .../presentation/widgets/cell/text_cell.dart | 6 +- .../widgets/cell/url_cell/url_cell.dart | 12 +- .../presentation/widgets/row/grid_row.dart | 16 +- .../presentation/widgets/row/row_detail.dart | 6 +- .../flowy-grid/src/entities/field_entities.rs | 9 - 20 files changed, 501 insertions(+), 197 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart create mode 100644 frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart rename frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/{cell_cotainer.dart => cell_container.dart} (100%) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 612e7c6770..953587852a 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,17 +1,161 @@ // ignore_for_file: unused_field +import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; -class BoardPage extends StatelessWidget { +class BoardPage extends StatefulWidget { final ViewPB _view; const BoardPage({required ViewPB view, Key? key}) : _view = view, super(key: key); + @override + State createState() => _BoardPageState(); +} + +class _BoardPageState extends State { + final BoardDataController boardDataController = BoardDataController( + onMoveColumn: (fromIndex, toIndex) { + debugPrint('Move column from $fromIndex to $toIndex'); + }, + onMoveColumnItem: (columnId, fromIndex, toIndex) { + debugPrint('Move $columnId:$fromIndex to $columnId:$toIndex'); + }, + onMoveColumnItemToColumn: (fromColumnId, fromIndex, toColumnId, toIndex) { + debugPrint('Move $fromColumnId:$fromIndex to $toColumnId:$toIndex'); + }, + ); + + @override + void initState() { + final column1 = BoardColumnData(id: "To Do", items: [ + TextItem("Card 1"), + TextItem("Card 2"), + RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + TextItem("Card 4"), + ]); + final column2 = BoardColumnData(id: "In Progress", items: [ + RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), + TextItem("Card 6"), + ]); + + final column3 = BoardColumnData(id: "Done", items: []); + + boardDataController.addColumn(column1); + boardDataController.addColumn(column2); + boardDataController.addColumn(column3); + super.initState(); + } + @override Widget build(BuildContext context) { - return Container(); + final config = BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ); + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Board( + dataController: boardDataController, + footBuilder: (context, columnData) { + return AppFlowyColumnFooter( + icon: const Icon(Icons.add, size: 20), + title: const Text('New'), + height: 50, + margin: config.columnItemPadding, + ); + }, + headerBuilder: (context, columnData) { + return AppFlowyColumnHeader( + icon: const Icon(Icons.lightbulb_circle), + title: Text(columnData.id), + addIcon: const Icon(Icons.add, size: 20), + moreIcon: const Icon(Icons.more_horiz, size: 20), + height: 50, + margin: config.columnItemPadding, + ); + }, + cardBuilder: (context, item) { + return AppFlowyColumnItemCard( + key: ObjectKey(item), + child: _buildCard(item), + ); + }, + columnConstraints: const BoxConstraints.tightFor(width: 240), + config: BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), + ), + ); + } + + Widget _buildCard(ColumnItem item) { + if (item is TextItem) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text(item.s), + ), + ); + } + + if (item is RichTextItem) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + Text( + item.subtitle, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ) + ], + ), + ), + ); + } + + throw UnimplementedError(); + } +} + +class TextItem extends ColumnItem { + final String s; + + TextItem(this.s); + + @override + String get id => s; +} + +class RichTextItem extends ColumnItem { + final String title; + final String subtitle; + + RichTextItem({required this.title, required this.subtitle}); + + @override + String get id => title; +} + +extension HexColor on Color { + static Color fromHex(String hexString) { + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString.replaceFirst('#', '')); + return Color(int.parse(buffer.toString(), radix: 16)); } } diff --git a/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart b/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart index 52c422b64c..bd4b651da8 100644 --- a/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart +++ b/frontend/app_flowy/lib/plugins/doc/presentation/banner.dart @@ -11,7 +11,9 @@ import 'package:app_flowy/generated/locale_keys.g.dart'; class DocumentBanner extends StatelessWidget { final void Function() onRestore; final void Function() onDelete; - const DocumentBanner({required this.onRestore, required this.onDelete, Key? key}) : super(key: key); + const DocumentBanner( + {required this.onRestore, required this.onDelete, Key? key}) + : super(key: key); @override Widget build(BuildContext context) { @@ -26,7 +28,8 @@ class DocumentBanner extends StatelessWidget { fit: BoxFit.scaleDown, child: Row( children: [ - FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(), color: Colors.white), + FlowyText.medium(LocaleKeys.deletePagePrompt_text.tr(), + color: Colors.white), const HSpace(20), BaseStyledButton( minWidth: 160, @@ -37,7 +40,10 @@ class DocumentBanner extends StatelessWidget { downColor: theme.main1, outlineColor: Colors.white, borderRadius: Corners.s8Border, - child: FlowyText.medium(LocaleKeys.deletePagePrompt_restore.tr(), color: Colors.white, fontSize: 14), + child: FlowyText.medium( + LocaleKeys.deletePagePrompt_restore.tr(), + color: Colors.white, + fontSize: 14), onPressed: onRestore), const HSpace(20), BaseStyledButton( @@ -49,8 +55,10 @@ class DocumentBanner extends StatelessWidget { downColor: theme.main1, outlineColor: Colors.white, borderRadius: Corners.s8Border, - child: FlowyText.medium(LocaleKeys.deletePagePrompt_deletePermanent.tr(), - color: Colors.white, fontSize: 14), + child: FlowyText.medium( + LocaleKeys.deletePagePrompt_deletePermanent.tr(), + color: Colors.white, + fontSize: 14), onPressed: onDelete), ], ), diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart index 72f1bc787d..950832c674 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; import 'cell_service.dart'; -abstract class GridFieldChangedNotifier { +abstract class IGridFieldChangedNotifier { void onFieldChanged(void Function(GridFieldPB) callback); void dispose(); } @@ -12,9 +12,10 @@ abstract class GridFieldChangedNotifier { /// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen. class GridCellFieldNotifier { /// fieldId: {objectId: callback} - final Map>> _fieldListenerByFieldId = {}; + final Map>> _fieldListenerByFieldId = + {}; - GridCellFieldNotifier({required GridFieldChangedNotifier notifier}) { + GridCellFieldNotifier({required IGridFieldChangedNotifier notifier}) { notifier.onFieldChanged( (field) { final map = _fieldListenerByFieldId[field.id]; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 8f78793a2c..526213ee4d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -246,7 +246,7 @@ class IGridCellController extends Equatable { } /// Save the cell data to disk - /// You can set [dedeplicate] to true (default is false) to reduce the save operation. + /// You can set [deduplicate] to true (default is false) to reduce the save operation. /// It's useful when you call this method when user editing the [TextField]. /// The default debounce interval is 300 milliseconds. void saveCellData(D data, @@ -304,7 +304,7 @@ class IGridCellController extends Equatable { [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id]; } -class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier { +class _GridFieldChangedNotifierImpl extends IGridFieldChangedNotifier { final GridFieldCache _cache; FieldChangesetCallback? _onChangesetFn; diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart index 19c4049224..4516d01ba3 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart @@ -1,40 +1,23 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; -import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'block/block_cache.dart'; -import 'grid_service.dart'; +import 'grid_data_controller.dart'; import 'row/row_service.dart'; import 'dart:collection'; part 'grid_bloc.freezed.dart'; class GridBloc extends Bloc { - final String gridId; - final GridService _gridService; - final GridFieldCache fieldCache; - - // key: the block id - final LinkedHashMap _blocks; - - List get rowInfos { - final List rows = []; - for (var block in _blocks.values) { - rows.addAll(block.rows); - } - return rows; - } + final GridDataController dataController; GridBloc({required ViewPB view}) - : gridId = view.id, - _blocks = LinkedHashMap.identity(), - _gridService = GridService(gridId: view.id), - fieldCache = GridFieldCache(gridId: view.id), + : dataController = GridDataController(view: view), super(GridState.initial(view.id)) { on( (event, emit) async { @@ -44,13 +27,21 @@ class GridBloc extends Bloc { await _loadGrid(emit); }, createRow: () { - _gridService.createRow(); + dataController.createRow(); }, - didReceiveRowUpdate: (newRowInfos, reason) { - emit(state.copyWith(rowInfos: newRowInfos, reason: reason)); + didReceiveGridUpdate: (grid) { + emit(state.copyWith(grid: Some(grid))); }, didReceiveFieldUpdate: (fields) { - emit(state.copyWith(rowInfos: rowInfos, fields: GridFieldEquatable(fields))); + emit(state.copyWith( + fields: GridFieldEquatable(fields), + )); + }, + didReceiveRowUpdate: (newRowInfos, reason) { + emit(state.copyWith( + rowInfos: newRowInfos, + reason: reason, + )); }, ); }, @@ -59,89 +50,63 @@ class GridBloc extends Bloc { @override Future close() async { - await _gridService.closeGrid(); - await fieldCache.dispose(); - - for (final blockCache in _blocks.values) { - blockCache.dispose(); - } + await dataController.dispose(); return super.close(); } GridRowCache? getRowCache(String blockId, String rowId) { - final GridBlockCache? blockCache = _blocks[blockId]; + final GridBlockCache? blockCache = dataController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - fieldCache.addListener( - listenWhen: () => !isClosed, - onFields: (fields) => add(GridEvent.didReceiveFieldUpdate(fields)), + dataController.addListener( + onGridChanged: (grid) { + if (!isClosed) { + add(GridEvent.didReceiveGridUpdate(grid)); + } + }, + onRowsChanged: (rowInfos, reason) { + if (!isClosed) { + add(GridEvent.didReceiveRowUpdate(rowInfos, reason)); + } + }, + onFieldsChanged: (fields) { + if (!isClosed) { + add(GridEvent.didReceiveFieldUpdate(fields)); + } + }, ); } Future _loadGrid(Emitter emit) async { - final result = await _gridService.loadGrid(); - return Future( - () => result.fold( - (grid) async { - _initialBlocks(grid.blocks); - await _loadFields(grid, emit); - }, - (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))), + final result = await dataController.loadData(); + result.fold( + (grid) => emit( + state.copyWith(loadingState: GridLoadingState.finish(left(unit))), + ), + (err) => emit( + state.copyWith(loadingState: GridLoadingState.finish(right(err))), ), ); } - - Future _loadFields(GridPB grid, Emitter emit) async { - final result = await _gridService.getFields(fieldIds: grid.fields); - return Future( - () => result.fold( - (fields) { - fieldCache.fields = fields.items; - - emit(state.copyWith( - grid: Some(grid), - fields: GridFieldEquatable(fieldCache.fields), - rowInfos: rowInfos, - loadingState: GridLoadingState.finish(left(unit)), - )); - }, - (err) => emit(state.copyWith(loadingState: GridLoadingState.finish(right(err)))), - ), - ); - } - - void _initialBlocks(List blocks) { - for (final block in blocks) { - if (_blocks[block.id] != null) { - Log.warn("Intial duplicate block's cache: ${block.id}"); - return; - } - - final cache = GridBlockCache( - gridId: gridId, - block: block, - fieldCache: fieldCache, - ); - - cache.addListener( - listenWhen: () => !isClosed, - onChangeReason: (reason) => add(GridEvent.didReceiveRowUpdate(rowInfos, reason)), - ); - - _blocks[block.id] = cache; - } - } } @freezed class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; - const factory GridEvent.didReceiveRowUpdate(List rows, GridRowChangeReason listState) = - _DidReceiveRowUpdate; - const factory GridEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; + const factory GridEvent.didReceiveRowUpdate( + List rows, + GridRowChangeReason listState, + ) = _DidReceiveRowUpdate; + const factory GridEvent.didReceiveFieldUpdate( + UnmodifiableListView fields, + ) = _DidReceiveFieldUpdate; + + const factory GridEvent.didReceiveGridUpdate( + GridPB grid, + ) = _DidReceiveGridUpdate; } @freezed @@ -156,7 +121,7 @@ class GridState with _$GridState { }) = _GridState; factory GridState.initial(String gridId) => GridState( - fields: const GridFieldEquatable([]), + fields: GridFieldEquatable(UnmodifiableListView([])), rowInfos: [], grid: none(), gridId: gridId, @@ -168,18 +133,27 @@ class GridState with _$GridState { @freezed class GridLoadingState with _$GridLoadingState { const factory GridLoadingState.loading() = _Loading; - const factory GridLoadingState.finish(Either successOrFail) = _Finish; + const factory GridLoadingState.finish( + Either successOrFail) = _Finish; } class GridFieldEquatable extends Equatable { - final List _fields; - const GridFieldEquatable(List fields) : _fields = fields; + final UnmodifiableListView _fields; + const GridFieldEquatable( + UnmodifiableListView fields, + ) : _fields = fields; @override List get props { + if (_fields.isEmpty) { + return []; + } + return [ _fields.length, - _fields.map((field) => field.width).reduce((value, element) => value + element), + _fields + .map((field) => field.width) + .reduce((value, element) => value + element), ]; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart new file mode 100644 index 0000000000..3563a13528 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -0,0 +1,128 @@ +import 'dart:collection'; + +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; +import 'dart:async'; +import 'package:dartz/dartz.dart'; +import 'block/block_cache.dart'; +import 'prelude.dart'; + +typedef OnFieldsChanged = void Function(UnmodifiableListView); +typedef OnGridChanged = void Function(GridPB); + +typedef OnRowsChanged = void Function( + List rowInfos, + GridRowChangeReason, +); +typedef ListenONRowChangedCondition = bool Function(); + +class GridDataController { + final String gridId; + final GridService _gridFFIService; + final GridFieldCache fieldCache; + + // key: the block id + final LinkedHashMap _blocks; + UnmodifiableMapView get blocks => + UnmodifiableMapView(_blocks); + + OnRowsChanged? _onRowChanged; + OnFieldsChanged? _onFieldsChanged; + OnGridChanged? _onGridChanged; + + List get rowInfos { + final List rows = []; + for (var block in _blocks.values) { + rows.addAll(block.rows); + } + return rows; + } + + GridDataController({required ViewPB view}) + : gridId = view.id, + _blocks = LinkedHashMap.identity(), + _gridFFIService = GridService(gridId: view.id), + fieldCache = GridFieldCache(gridId: view.id); + + void addListener({ + required OnGridChanged onGridChanged, + required OnRowsChanged onRowsChanged, + required OnFieldsChanged onFieldsChanged, + }) { + _onGridChanged = onGridChanged; + _onRowChanged = onRowsChanged; + _onFieldsChanged = onFieldsChanged; + + fieldCache.addListener(onFields: (fields) { + _onFieldsChanged?.call(UnmodifiableListView(fields)); + }); + } + + Future> loadData() async { + final result = await _gridFFIService.loadGrid(); + return Future( + () => result.fold( + (grid) async { + _initialBlocks(grid.blocks); + _onGridChanged?.call(grid); + return await _loadFields(grid); + }, + (err) => right(err), + ), + ); + } + + void createRow() { + _gridFFIService.createRow(); + } + + Future dispose() async { + await _gridFFIService.closeGrid(); + await fieldCache.dispose(); + + for (final blockCache in _blocks.values) { + blockCache.dispose(); + } + } + + void _initialBlocks(List blocks) { + for (final block in blocks) { + if (_blocks[block.id] != null) { + Log.warn("Initial duplicate block's cache: ${block.id}"); + return; + } + + final cache = GridBlockCache( + gridId: gridId, + block: block, + fieldCache: fieldCache, + ); + + cache.addListener( + onChangeReason: (reason) { + _onRowChanged?.call(rowInfos, reason); + }, + ); + + _blocks[block.id] = cache; + } + } + + Future> _loadFields(GridPB grid) async { + final result = await _gridFFIService.getFields(fieldIds: grid.fields); + return Future( + () => result.fold( + (fields) { + fieldCache.fields = fields.items; + _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields)); + return left(unit); + }, + (err) => right(err), + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index 3b755d1524..b6079b6764 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -5,25 +5,25 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; +import 'row_data_controller.dart'; import 'row_service.dart'; part 'row_bloc.freezed.dart'; class RowBloc extends Bloc { final RowService _rowService; - final GridRowCache _rowCache; - void Function()? _rowListenFn; + final GridRowDataController _dataController; RowBloc({ required GridRowInfo rowInfo, - required GridRowCache rowCache, + required GridRowDataController dataController, }) : _rowService = RowService( gridId: rowInfo.gridId, blockId: rowInfo.blockId, rowId: rowInfo.id, ), - _rowCache = rowCache, - super(RowState.initial(rowInfo, rowCache.loadGridCells(rowInfo.id))) { + _dataController = dataController, + super(RowState.initial(rowInfo, dataController.loadData())) { on( (event, emit) async { await event.map( @@ -33,7 +33,7 @@ class RowBloc extends Bloc { createRow: (_CreateRow value) { _rowService.createRow(); }, - didReceiveCellDatas: (_DidReceiveCellDatas value) async { + didReceiveCells: (_DidReceiveCells value) async { final fields = value.gridCellMap.values .map((e) => GridCellEquatable(e.field)) .toList(); @@ -51,19 +51,17 @@ class RowBloc extends Bloc { @override Future close() async { - if (_rowListenFn != null) { - _rowCache.removeRowListener(_rowListenFn!); - } - + _dataController.dispose(); return super.close(); } Future _startListening() async { - _rowListenFn = _rowCache.addListener( - rowId: state.rowInfo.id, - onCellUpdated: (cellDatas, reason) => - add(RowEvent.didReceiveCellDatas(cellDatas, reason)), - listenWhen: () => !isClosed, + _dataController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(RowEvent.didReceiveCells(cells, reason)); + } + }, ); } } @@ -72,9 +70,8 @@ class RowBloc extends Bloc { class RowEvent with _$RowEvent { const factory RowEvent.initial() = _InitialRow; const factory RowEvent.createRow() = _CreateRow; - const factory RowEvent.didReceiveCellDatas( - GridCellMap gridCellMap, GridRowChangeReason reason) = - _DidReceiveCellDatas; + const factory RowEvent.didReceiveCells( + GridCellMap gridCellMap, GridRowChangeReason reason) = _DidReceiveCells; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart new file mode 100644 index 0000000000..fb05ff5920 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import '../cell/cell_service/cell_service.dart'; +import '../grid_service.dart'; +import 'row_service.dart'; + +typedef OnRowChanged = void Function(GridCellMap, GridRowChangeReason); + +class GridRowDataController { + final String rowId; + VoidCallback? _onRowChangedListener; + final GridFieldCache _fieldCache; + final GridRowCache _rowCache; + + GridFieldCache get fieldCache => _fieldCache; + + GridRowCache get rowCache => _rowCache; + + GridRowDataController({ + required this.rowId, + required GridFieldCache fieldCache, + required GridRowCache rowCache, + }) : _fieldCache = fieldCache, + _rowCache = rowCache; + + GridCellMap loadData() { + return _rowCache.loadGridCells(rowId); + } + + void addListener({OnRowChanged? onRowChanged}) { + _onRowChangedListener = _rowCache.addListener( + rowId: rowId, + onCellUpdated: onRowChanged, + ); + } + + void dispose() { + if (_onRowChangedListener != null) { + _rowCache.removeRowListener(_onRowChangedListener!); + } + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index 4fd18a1fdd..d72a05089a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -158,7 +158,7 @@ class GridRowCache { void Function(GridCellMap, GridRowChangeReason)? onCellUpdated, bool Function()? listenWhen, }) { - listenrHandler() async { + listenerHandler() async { if (listenWhen != null && listenWhen() == false) { return; } @@ -181,8 +181,8 @@ class GridRowCache { ); } - _rowChangeReasonNotifier.addListener(listenrHandler); - return listenrHandler; + _rowChangeReasonNotifier.addListener(listenerHandler); + return listenerHandler; } void removeRowListener(VoidCallback callback) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/controller/grid_scroll.dart b/frontend/app_flowy/lib/plugins/grid/presentation/controller/grid_scroll.dart index dddd93d175..72b5152aea 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/controller/grid_scroll.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/controller/grid_scroll.dart @@ -2,19 +2,20 @@ import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; class GridScrollController { - final LinkedScrollControllerGroup _scrollGroupContorller; + final LinkedScrollControllerGroup _scrollGroupController; final ScrollController verticalController; final ScrollController horizontalController; final List _linkHorizontalControllers = []; - GridScrollController({required LinkedScrollControllerGroup scrollGroupContorller}) - : _scrollGroupContorller = scrollGroupContorller, + GridScrollController( + {required LinkedScrollControllerGroup scrollGroupController}) + : _scrollGroupController = scrollGroupController, verticalController = ScrollController(), - horizontalController = scrollGroupContorller.addAndGet(); + horizontalController = scrollGroupController.addAndGet(); ScrollController linkHorizontalController() { - final controller = _scrollGroupContorller.addAndGet(); + final controller = _scrollGroupController.addAndGet(); _linkHorizontalControllers.add(controller); return controller; } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index ed6306a972..d267e4aca9 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -1,3 +1,4 @@ +import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; @@ -79,7 +80,7 @@ class FlowyGrid extends StatefulWidget { class _FlowyGridState extends State { final _scrollController = GridScrollController( - scrollGroupContorller: LinkedScrollControllerGroup()); + scrollGroupController: LinkedScrollControllerGroup()); late ScrollController headerScrollController; @override @@ -153,7 +154,7 @@ class _FlowyGridState extends State { } Widget _gridHeader(BuildContext context, String gridId) { - final fieldCache = context.read().fieldCache; + final fieldCache = context.read().dataController.fieldCache; return GridHeaderSliverAdaptor( gridId: gridId, fieldCache: fieldCache, @@ -169,7 +170,7 @@ class _GridToolbarAdaptor extends StatelessWidget { Widget build(BuildContext context) { return BlocSelector( selector: (state) { - final fieldCache = context.read().fieldCache; + final fieldCache = context.read().dataController.fieldCache; return GridToolbarContext( gridId: state.gridId, fieldCache: fieldCache, @@ -237,14 +238,20 @@ class _GridRowsState extends State<_GridRows> { ) { final rowCache = context.read().getRowCache(rowInfo.blockId, rowInfo.id); - final fieldCache = context.read().fieldCache; + + final fieldCache = context.read().dataController.fieldCache; if (rowCache != null) { + final dataController = GridRowDataController( + rowId: rowInfo.id, + fieldCache: fieldCache, + rowCache: rowCache, + ); + return SizeTransition( sizeFactor: animation, child: GridRowWidget( rowData: rowInfo, - rowCache: rowCache, - fieldCache: fieldCache, + dataController: dataController, key: ValueKey(rowInfo.id), ), ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart index 1913aac786..c53bee8423 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart @@ -27,39 +27,49 @@ class GridCellBuilder { cellCache: cellCache, fieldCache: fieldCache, ); + final key = cell.key(); switch (cell.fieldType) { case FieldType.Checkbox: return GridCheckboxCell( - cellControllerBuilder: cellControllerBuilder, key: key); + cellControllerBuilder: cellControllerBuilder, + key: key, + ); case FieldType.DateTime: return GridDateCell( - cellControllerBuilder: cellControllerBuilder, - key: key, - style: style); + cellControllerBuilder: cellControllerBuilder, + key: key, + style: style, + ); case FieldType.SingleSelect: return GridSingleSelectCell( - cellContorllerBuilder: cellControllerBuilder, - style: style, - key: key); + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); case FieldType.MultiSelect: return GridMultiSelectCell( - cellContorllerBuilder: cellControllerBuilder, - style: style, - key: key); + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); case FieldType.Number: return GridNumberCell( - cellContorllerBuilder: cellControllerBuilder, key: key); + cellControllerBuilder: cellControllerBuilder, + key: key, + ); case FieldType.RichText: return GridTextCell( - cellContorllerBuilder: cellControllerBuilder, - style: style, - key: key); + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); case FieldType.URL: return GridURLCell( - cellContorllerBuilder: cellControllerBuilder, - style: style, - key: key); + cellControllerBuilder: cellControllerBuilder, + style: style, + key: key, + ); } throw UnimplementedError; } @@ -93,7 +103,7 @@ abstract class GridCellWidget extends StatefulWidget @override final ValueNotifier onCellFocus = ValueNotifier(false); - // When the cell is focused, we assume that the accessory alse be hovered. + // When the cell is focused, we assume that the accessory also be hovered. @override ValueNotifier get onAccessoryHover => onCellFocus; @@ -150,7 +160,7 @@ abstract class GridCellState extends State { abstract class GridFocusNodeCellState extends GridCellState { - SingleListenrFocusNode focusNode = SingleListenrFocusNode(); + SingleListenerFocusNode focusNode = SingleListenerFocusNode(); @override void initState() { @@ -219,7 +229,7 @@ class GridCellFocusListener extends ChangeNotifier { abstract class GridCellStyle {} -class SingleListenrFocusNode extends FocusNode { +class SingleListenerFocusNode extends FocusNode { VoidCallback? _listener; void setListener(VoidCallback listener) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_cotainer.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart similarity index 100% rename from frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_cotainer.dart rename to frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart index 8c6db0afe9..a24243ef99 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart @@ -7,10 +7,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'cell_builder.dart'; class GridNumberCell extends GridCellWidget { - final GridCellControllerBuilder cellContorllerBuilder; + final GridCellControllerBuilder cellControllerBuilder; GridNumberCell({ - required this.cellContorllerBuilder, + required this.cellControllerBuilder, Key? key, }) : super(key: key); @@ -25,7 +25,7 @@ class _NumberCellState extends GridFocusNodeCellState { @override void initState() { - final cellContext = widget.cellContorllerBuilder.build(); + final cellContext = widget.cellControllerBuilder.build(); _cellBloc = getIt(param1: cellContext) ..add(const NumberCellEvent.initial()); _controller = diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index 53c77c6016..f4ebfa8d4d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -22,11 +22,11 @@ class SelectOptionCellStyle extends GridCellStyle { } class GridSingleSelectCell extends GridCellWidget { - final GridCellControllerBuilder cellContorllerBuilder; + final GridCellControllerBuilder cellControllerBuilder; late final SelectOptionCellStyle? cellStyle; GridSingleSelectCell({ - required this.cellContorllerBuilder, + required this.cellControllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -47,7 +47,7 @@ class _SingleSelectCellState extends State { @override void initState() { final cellContext = - widget.cellContorllerBuilder.build() as GridSelectOptionCellController; + widget.cellControllerBuilder.build() as GridSelectOptionCellController; _cellBloc = getIt(param1: cellContext) ..add(const SelectOptionCellEvent.initial()); super.initState(); @@ -63,7 +63,7 @@ class _SingleSelectCellState extends State { selectOptions: state.selectedOptions, cellStyle: widget.cellStyle, onFocus: (value) => widget.onCellEditing.value = value, - cellContorllerBuilder: widget.cellContorllerBuilder); + cellControllerBuilder: widget.cellControllerBuilder); }, ), ); @@ -78,11 +78,11 @@ class _SingleSelectCellState extends State { //---------------------------------------------------------------- class GridMultiSelectCell extends GridCellWidget { - final GridCellControllerBuilder cellContorllerBuilder; + final GridCellControllerBuilder cellControllerBuilder; late final SelectOptionCellStyle? cellStyle; GridMultiSelectCell({ - required this.cellContorllerBuilder, + required this.cellControllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -103,7 +103,7 @@ class _MultiSelectCellState extends State { @override void initState() { final cellContext = - widget.cellContorllerBuilder.build() as GridSelectOptionCellController; + widget.cellControllerBuilder.build() as GridSelectOptionCellController; _cellBloc = getIt(param1: cellContext) ..add(const SelectOptionCellEvent.initial()); super.initState(); @@ -119,7 +119,7 @@ class _MultiSelectCellState extends State { selectOptions: state.selectedOptions, cellStyle: widget.cellStyle, onFocus: (value) => widget.onCellEditing.value = value, - cellContorllerBuilder: widget.cellContorllerBuilder); + cellControllerBuilder: widget.cellControllerBuilder); }, ), ); @@ -136,12 +136,12 @@ class _SelectOptionCell extends StatelessWidget { final List selectOptions; final void Function(bool) onFocus; final SelectOptionCellStyle? cellStyle; - final GridCellControllerBuilder cellContorllerBuilder; + final GridCellControllerBuilder cellControllerBuilder; const _SelectOptionCell({ required this.selectOptions, required this.onFocus, required this.cellStyle, - required this.cellContorllerBuilder, + required this.cellControllerBuilder, Key? key, }) : super(key: key); @@ -179,7 +179,7 @@ class _SelectOptionCell extends StatelessWidget { onTap: () { onFocus(true); final cellContext = - cellContorllerBuilder.build() as GridSelectOptionCellController; + cellControllerBuilder.build() as GridSelectOptionCellController; SelectOptionCellEditor.show( context, cellContext, () => onFocus(false)); }, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart index 29471c13b6..21f9c60631 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart @@ -14,10 +14,10 @@ class GridTextCellStyle extends GridCellStyle { } class GridTextCell extends GridCellWidget { - final GridCellControllerBuilder cellContorllerBuilder; + final GridCellControllerBuilder cellControllerBuilder; late final GridTextCellStyle? cellStyle; GridTextCell({ - required this.cellContorllerBuilder, + required this.cellControllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -39,7 +39,7 @@ class _GridTextCellState extends GridFocusNodeCellState { @override void initState() { - final cellContext = widget.cellContorllerBuilder.build(); + final cellContext = widget.cellControllerBuilder.build(); _cellBloc = getIt(param1: cellContext); _cellBloc.add(const TextCellEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index bb39b8f276..506cc3bc2b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -31,10 +31,10 @@ enum GridURLCellAccessoryType { } class GridURLCell extends GridCellWidget { - final GridCellControllerBuilder cellContorllerBuilder; + final GridCellControllerBuilder cellControllerBuilder; late final GridURLCellStyle? cellStyle; GridURLCell({ - required this.cellContorllerBuilder, + required this.cellControllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -53,14 +53,14 @@ class GridURLCell extends GridCellWidget { switch (ty) { case GridURLCellAccessoryType.edit: final cellContext = - cellContorllerBuilder.build() as GridURLCellController; + cellControllerBuilder.build() as GridURLCellController; return _EditURLAccessory( cellContext: cellContext, anchorContext: buildContext.anchorContext); case GridURLCellAccessoryType.copyURL: final cellContext = - cellContorllerBuilder.build() as GridURLCellController; + cellControllerBuilder.build() as GridURLCellController; return _CopyURLAccessory(cellContext: cellContext); } } @@ -91,7 +91,7 @@ class _GridURLCellState extends GridCellState { @override void initState() { final cellContext = - widget.cellContorllerBuilder.build() as GridURLCellController; + widget.cellControllerBuilder.build() as GridURLCellController; _cellBloc = URLCellBloc(cellContext: cellContext); _cellBloc.add(const URLCellEvent.initial()); super.initState(); @@ -141,7 +141,7 @@ class _GridURLCellState extends GridCellState { await launchUrl(uri); } else { final cellContext = - widget.cellContorllerBuilder.build() as GridURLCellController; + widget.cellControllerBuilder.build() as GridURLCellController; widget.onCellEditing.value = true; URLCellEditor.show(context, cellContext, () { widget.onCellEditing.value = false; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index effc1b188c..d37a45d210 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -9,24 +10,23 @@ import 'package:provider/provider.dart'; import '../../layout/sizes.dart'; import '../cell/cell_accessory.dart'; -import '../cell/cell_cotainer.dart'; +import '../cell/cell_container.dart'; import '../cell/prelude.dart'; import 'row_action_sheet.dart'; import 'row_detail.dart'; class GridRowWidget extends StatefulWidget { final GridRowInfo rowData; - final GridRowCache rowCache; + final GridRowDataController dataController; final GridCellBuilder cellBuilder; GridRowWidget({ required this.rowData, - required this.rowCache, - required GridFieldCache fieldCache, + required this.dataController, Key? key, }) : cellBuilder = GridCellBuilder( - cellCache: rowCache.cellCache, - fieldCache: fieldCache, + cellCache: dataController.rowCache.cellCache, + fieldCache: dataController.fieldCache, ), super(key: key); @@ -41,7 +41,7 @@ class _GridRowWidgetState extends State { void initState() { _rowBloc = RowBloc( rowInfo: widget.rowData, - rowCache: widget.rowCache, + dataController: widget.dataController, ); _rowBloc.add(const RowEvent.initial()); super.initState(); @@ -81,7 +81,7 @@ class _GridRowWidgetState extends State { void _expandRow(BuildContext context) { final page = RowDetailPage( rowInfo: widget.rowData, - rowCache: widget.rowCache, + rowCache: widget.dataController.rowCache, cellBuilder: widget.cellBuilder, ); page.show(context); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index e7e00ead68..b45457fffd 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -62,8 +62,10 @@ class _RowDetailPageState extends State { Widget build(BuildContext context) { return BlocProvider( create: (context) { - final bloc = - RowDetailBloc(rowInfo: widget.rowInfo, rowCache: widget.rowCache); + final bloc = RowDetailBloc( + rowInfo: widget.rowInfo, + rowCache: widget.rowCache, + ); bloc.add(const RowDetailEvent.initial()); return bloc; }, diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index dce77787c3..dd31e22a54 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -164,18 +164,11 @@ pub struct CreateFieldPayloadPB { pub grid_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 CreateFieldParams { pub grid_id: String, - pub field_id: String, pub field_type: FieldType, } @@ -184,10 +177,8 @@ impl TryInto for CreateFieldPayloadPB { fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; - let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; Ok(CreateFieldParams { grid_id: grid_id.0, - field_id: field_id.0, field_type: self.field_type, }) } From 8fa55cfa08f3fe3203476c6f7510c40a1769216d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 18:19:11 +0800 Subject: [PATCH 020/224] feat: text insert and replace with selection styles --- .../lib/operation/transaction_builder.dart | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 8fa67687c2..55ca336bfb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'dart:math'; import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; @@ -105,7 +106,20 @@ class TransactionBuilder { insertText(TextNode node, int index, String content, [Attributes? attributes]) { - textEdit(node, () => Delta().retain(index).insert(content, attributes)); + textEdit( + node, + () => Delta().retain(index).insert( + content, + attributes ?? + (index == 0 + ? null + : node.delta + .slice(max(index - 1, 0), index) + .operations + .first + .attributes), + ), + ); afterSelection = Selection.collapsed( Position(path: node.path, offset: index + content.length)); } @@ -121,10 +135,19 @@ class TransactionBuilder { Selection.collapsed(Position(path: node.path, offset: index)); } - replaceText(TextNode node, int index, int length, String content) { + replaceText(TextNode node, int index, int length, String content, + [Attributes? attributes]) { textEdit( node, - () => Delta().retain(index).delete(length).insert(content), + () => Delta().retain(index).delete(length).insert( + content, + attributes ?? + node.delta + .slice(index, index + length) + .operations + .first + .attributes, + ), ); afterSelection = Selection.collapsed( Position( From 4223324689d0e65604f9569dd166960690ce9a98 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 18:54:33 +0800 Subject: [PATCH 021/224] fix: cursor height error --- .../flowy_editor/lib/render/rich_text/flowy_rich_text.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 882f98c4d5..81d9159d40 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -41,6 +41,8 @@ class _FlowyRichTextState extends State with Selectable { final _textKey = GlobalKey(); final _placeholderTextKey = GlobalKey(); + final lineHeight = 1.5; + RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @@ -145,6 +147,7 @@ class _FlowyRichTextState extends State with Selectable { ? Colors.transparent : Colors.grey, fontSize: baseFontSize, + height: lineHeight, ), ), ], @@ -200,6 +203,7 @@ class _FlowyRichTextState extends State with Selectable { .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, text: insert.content, + height: lineHeight, ).toTextSpan()) .toList(growable: false), ); From 1391d202a971216537a5b690f0223b3455d7ac76 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 19:03:55 +0800 Subject: [PATCH 022/224] fix: could not remove text style when pressing enter in empty text node --- .../lib/operation/transaction_builder.dart | 17 +++++---- .../lib/render/rich_text/rich_text_style.dart | 1 + ...er_without_shift_in_text_node_handler.dart | 35 +++++++++++++------ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 55ca336bfb..2898f7113a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -137,17 +137,16 @@ class TransactionBuilder { replaceText(TextNode node, int index, int length, String content, [Attributes? attributes]) { + var newAttributes = attributes; + if (attributes == null) { + final ops = node.delta.slice(index, index + length).operations; + if (ops.isNotEmpty) { + newAttributes = ops.first.attributes; + } + } textEdit( node, - () => Delta().retain(index).delete(length).insert( - content, - attributes ?? - node.delta - .slice(index, index + length) - .operations - .first - .attributes, - ), + () => Delta().retain(index).delete(length).insert(content, newAttributes), ); afterSelection = Selection.collapsed( Position( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index e39aed9aa2..c44fd8dac1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -51,6 +51,7 @@ class StyleKey { ]; static List globalStyleKeys = [ + StyleKey.subtype, StyleKey.heading, StyleKey.checkbox, StyleKey.bulletedList, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index d2d3b14450..3dd0eef2df 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -67,16 +67,31 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = // If selection is collapsed and position.start.offset == 0, // insert a empty text node before. if (selection.isCollapsed && selection.start.offset == 0) { - final afterSelection = Selection.collapsed( - Position(path: textNode.path.next, offset: 0), - ); - TransactionBuilder(editorState) - ..insertNode( - textNode.path, - TextNode.empty(), - ) - ..afterSelection = afterSelection - ..commit(); + if (textNode.toRawString().isEmpty) { + final afterSelection = Selection.collapsed( + Position(path: textNode.path, offset: 0), + ); + TransactionBuilder(editorState) + ..updateNode( + textNode, + Attributes.fromIterable( + StyleKey.globalStyleKeys, + value: (_) => null, + )) + ..afterSelection = afterSelection + ..commit(); + } else { + final afterSelection = Selection.collapsed( + Position(path: textNode.path.next, offset: 0), + ); + TransactionBuilder(editorState) + ..insertNode( + textNode.path, + TextNode.empty(), + ) + ..afterSelection = afterSelection + ..commit(); + } return KeyEventResult.handled; } From 0650c40d9d86c288534b88ce0c8fc318ec66dc52 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 19:11:17 +0800 Subject: [PATCH 023/224] fix: checkbox error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pressing Enter after a checked-off item, the new checkbox is also checked off. it should be unchecked when it’s newly created. --- .../enter_without_shift_in_text_node_handler.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 3dd0eef2df..51e593a20b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -100,6 +100,13 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = final needCopyAttributes = StyleKey.globalStyleKeys .where((key) => key != StyleKey.heading) .contains(textNode.subtype); + Attributes attributes = {}; + if (needCopyAttributes) { + attributes = Attributes.from(textNode.attributes); + if (attributes.check) { + attributes[StyleKey.checkbox] = false; + } + } final afterSelection = Selection.collapsed( Position(path: textNode.path.next, offset: 0), ); @@ -107,8 +114,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = ..insertNode( textNode.path.next, textNode.copyWith( - attributes: - needCopyAttributes ? Attributes.from(textNode.attributes) : {}, + attributes: attributes, delta: textNode.delta.slice(selection.end.offset), ), ) From 3e256be0b9d5435bef51a577f8894ebfb15b424d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 19:19:35 +0800 Subject: [PATCH 024/224] fix: checkbox placeholder error --- .../flowy_editor/lib/render/rich_text/checkbox_text.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 890f80fd54..073c339ed6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -94,6 +94,7 @@ class _CheckboxNodeWidgetState extends State placeholderText: 'To-do', textNode: widget.textNode, textSpanDecorator: _textSpanDecorator, + placeholderTextSpanDecorator: _textSpanDecorator, editorState: widget.editorState, ), ), From 215587a50703ff0e7103f3eae041e076c1aa5e57 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 19:37:46 +0800 Subject: [PATCH 025/224] chore: format code --- .../lib/operation/transaction_builder.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index 2898f7113a..cececec924 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -106,18 +106,19 @@ class TransactionBuilder { insertText(TextNode node, int index, String content, [Attributes? attributes]) { + var newAttributes = attributes; + if (index != 0 && attributes == null) { + newAttributes = node.delta + .slice(max(index - 1, 0), index) + .operations + .first + .attributes; + } textEdit( node, () => Delta().retain(index).insert( content, - attributes ?? - (index == 0 - ? null - : node.delta - .slice(max(index - 1, 0), index) - .operations - .first - .attributes), + newAttributes, ), ); afterSelection = Selection.collapsed( From 6a27c490aad0edc90bf0dcfb785f49304a895aa0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Aug 2022 19:43:28 +0800 Subject: [PATCH 026/224] fix: selection error in edge --- .../flowy_editor/lib/service/selection_service.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 1786eca05f..9ae0082001 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -339,9 +339,10 @@ class _FlowySelectionState extends State start: isDownward ? start : end, end: isDownward ? end : start); debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); editorState.updateCursorSelection(selection); + + _scrollUpOrDownIfNeeded(panEndOffset!, isDownward); } - _scrollUpOrDownIfNeeded(panEndOffset!); _showDebugLayerIfNeeded(); } @@ -466,7 +467,7 @@ class _FlowySelectionState extends State return NodeIterator(stateTree, startNode, endNode).toList(); } - void _scrollUpOrDownIfNeeded(Offset offset) { + void _scrollUpOrDownIfNeeded(Offset offset, bool isDownward) { final dy = editorState.service.scrollService?.dy; if (dy == null) { assert(false, 'Dy could not be null'); @@ -478,10 +479,10 @@ class _FlowySelectionState extends State /// TODO: It is necessary to calculate the relative speed /// according to the gap and move forward more gently. final distance = 10.0; - if (offset.dy <= topLimit) { + if (offset.dy <= topLimit && !isDownward) { // up editorState.service.scrollService?.scrollTo(dy - distance); - } else if (offset.dy >= bottomLimit) { + } else if (offset.dy >= bottomLimit && isDownward) { //down editorState.service.scrollService?.scrollTo(dy + distance); } From ab4a2e8b07af72bd9efbba5027e8b164707b224b Mon Sep 17 00:00:00 2001 From: Aryman Date: Wed, 10 Aug 2022 03:23:37 +0530 Subject: [PATCH 027/224] fix: menu displayed at mouse location --- .../menu/app/section/disclosure_action.dart | 19 ++++++++++++------- .../home/menu/app/section/item.dart | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart index 7fe02bab1a..7375f6a113 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart @@ -76,13 +76,7 @@ class ViewDisclosureRegion extends StatelessWidget @override Widget build(BuildContext context) { return Listener( - onPointerDown: (event) => { - if (event.kind == PointerDeviceKind.mouse && - event.buttons == kSecondaryMouseButton) - { - show(context), - } - }, + onPointerDown: (event) => {_handleClick(event, context)}, child: child, ); } @@ -101,6 +95,17 @@ class ViewDisclosureRegion extends StatelessWidget (wrapper) => onSelected(dartz.some(wrapper.inner)), ); }; + + void _handleClick(PointerDownEvent event, BuildContext context) { + if (event.kind == PointerDeviceKind.mouse && + event.buttons == kSecondaryMouseButton) { + RenderBox box = context.findRenderObject() as RenderBox; + Offset position = box.localToGlobal(Offset.zero); + double x = event.position.dx - position.dx - box.size.width; + double y = event.position.dy - position.dy - box.size.height; + show(context, anchorOffset: Offset(x, y)); + } + } } class ViewDisclosureActionWrapper extends ActionItem { diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index 93855c3fa2..3ca8577d8c 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -86,7 +86,6 @@ class ViewSectionItem extends StatelessWidget { } return ViewDisclosureRegion( - // context.read().add(const ViewEvent.setIsEditing(true)), onSelected: (action) { context.read().add(const ViewEvent.setIsEditing(false)); _handleAction(context, action); From b695ceb832bcdaf677a28d1f10a9c3295e597379 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 10:07:41 +0800 Subject: [PATCH 028/224] chore: seperate FolderRevision from FolderPad --- .../src/services/folder_editor.rs | 6 +- shared-lib/flowy-folder-data-model/Cargo.toml | 2 +- .../src/revision/folder_rev.rs | 9 + .../src/revision/mod.rs | 2 + .../flowy-sync/src/client_folder/builder.rs | 46 +- .../src/client_folder/folder_pad.rs | 139 +++--- .../src/client_grid/grid_meta_pad.rs | 432 ------------------ .../src/client_grid/grid_revision_pad.rs | 7 +- .../lib-ot/src/core/operation/builder.rs | 11 +- 9 files changed, 122 insertions(+), 532 deletions(-) create mode 100644 shared-lib/flowy-folder-data-model/src/revision/folder_rev.rs delete mode 100644 shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs diff --git a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs index de461379e1..af0ae132f1 100644 --- a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs +++ b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs @@ -6,7 +6,7 @@ use flowy_revision::{ }; use flowy_sync::util::make_delta_from_revisions; use flowy_sync::{ - client_folder::{FolderChange, FolderPad}, + client_folder::{FolderChangeset, FolderPad}, entities::{revision::Revision, ws_data::ServerRevisionWSData}, }; use lib_infra::future::FutureResult; @@ -77,8 +77,8 @@ impl FolderEditor { Ok(()) } - pub(crate) fn apply_change(&self, change: FolderChange) -> FlowyResult<()> { - let FolderChange { delta, md5 } = change; + pub(crate) fn apply_change(&self, change: FolderChangeset) -> FlowyResult<()> { + let FolderChangeset { delta, md5 } = change; let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); let delta_data = delta.json_bytes(); let revision = Revision::new( diff --git a/shared-lib/flowy-folder-data-model/Cargo.toml b/shared-lib/flowy-folder-data-model/Cargo.toml index bacd1c9509..15b8e47bac 100644 --- a/shared-lib/flowy-folder-data-model/Cargo.toml +++ b/shared-lib/flowy-folder-data-model/Cargo.toml @@ -17,7 +17,7 @@ log = "0.4.14" nanoid = "0.4.0" chrono = { version = "0.4" } flowy-error-code = { path = "../flowy-error-code"} -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" serde_repr = "0.1" diff --git a/shared-lib/flowy-folder-data-model/src/revision/folder_rev.rs b/shared-lib/flowy-folder-data-model/src/revision/folder_rev.rs new file mode 100644 index 0000000000..f7d48b93fd --- /dev/null +++ b/shared-lib/flowy-folder-data-model/src/revision/folder_rev.rs @@ -0,0 +1,9 @@ +use crate::revision::{TrashRevision, WorkspaceRevision}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Default, Deserialize, Serialize, Clone, Eq, PartialEq)] +pub struct FolderRevision { + pub workspaces: Vec>, + pub trash: Vec>, +} diff --git a/shared-lib/flowy-folder-data-model/src/revision/mod.rs b/shared-lib/flowy-folder-data-model/src/revision/mod.rs index 6a837ac035..a1e9d5e33e 100644 --- a/shared-lib/flowy-folder-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-folder-data-model/src/revision/mod.rs @@ -1,9 +1,11 @@ mod app_rev; +mod folder_rev; mod trash_rev; mod view_rev; mod workspace_rev; pub use app_rev::*; +pub use folder_rev::*; pub use trash_rev::*; pub use view_rev::*; pub use workspace_rev::*; diff --git a/shared-lib/flowy-sync/src/client_folder/builder.rs b/shared-lib/flowy-sync/src/client_folder/builder.rs index 3855d62834..75a1343570 100644 --- a/shared-lib/flowy-sync/src/client_folder/builder.rs +++ b/shared-lib/flowy-sync/src/client_folder/builder.rs @@ -3,18 +3,17 @@ use crate::util::make_delta_from_revisions; use crate::{ client_folder::{default_folder_delta, FolderPad}, entities::revision::Revision, - errors::{CollaborateError, CollaborateResult}, + errors::CollaborateResult, }; use flowy_folder_data_model::revision::{TrashRevision, WorkspaceRevision}; -use lib_ot::core::{PhantomAttributes, TextDelta, TextDeltaBuilder}; +use lib_ot::core::PhantomAttributes; use serde::{Deserialize, Serialize}; -use std::sync::Arc; #[derive(Serialize, Deserialize)] pub(crate) struct FolderPadBuilder { - workspaces: Vec>, - trash: Vec>, + workspaces: Vec, + trash: Vec, } impl FolderPadBuilder { @@ -25,43 +24,28 @@ impl FolderPadBuilder { } } + #[allow(dead_code)] pub(crate) fn with_workspace(mut self, workspaces: Vec) -> Self { - self.workspaces = workspaces.into_iter().map(Arc::new).collect(); + self.workspaces = workspaces; self } + #[allow(dead_code)] pub(crate) fn with_trash(mut self, trash: Vec) -> Self { - self.trash = trash.into_iter().map(Arc::new).collect::>(); + self.trash = trash; self } - pub(crate) fn build_with_delta(self, mut delta: TextDelta) -> CollaborateResult { - if delta.is_empty() { - delta = default_folder_delta(); - } - - // TODO: Reconvert from history if delta.to_str() failed. - let content = delta.content()?; - let mut folder: FolderPad = serde_json::from_str(&content).map_err(|e| { - tracing::error!("Deserialize folder from {} failed", content); - return CollaborateError::internal().context(format!("Deserialize delta to folder failed: {}", e)); - })?; - folder.delta = delta; - Ok(folder) - } - pub(crate) fn build_with_revisions(self, revisions: Vec) -> CollaborateResult { - let folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; - self.build_with_delta(folder_delta) + let mut folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; + if folder_delta.is_empty() { + folder_delta = default_folder_delta(); + } + FolderPad::from_delta(folder_delta) } + #[allow(dead_code)] pub(crate) fn build(self) -> CollaborateResult { - let json = serde_json::to_string(&self) - .map_err(|e| CollaborateError::internal().context(format!("Serialize to folder json str failed: {}", e)))?; - Ok(FolderPad { - workspaces: self.workspaces, - trash: self.trash, - delta: TextDeltaBuilder::new().insert(&json).build(), - }) + FolderPad::new(self.workspaces, self.trash) } } diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index 5927be2c34..278c0970ff 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -8,26 +8,33 @@ use crate::{ }, errors::{CollaborateError, CollaborateResult}, }; -use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision, WorkspaceRevision}; +use flowy_folder_data_model::revision::{AppRevision, FolderRevision, TrashRevision, ViewRevision, WorkspaceRevision}; use lib_infra::util::move_vec_element; use lib_ot::core::*; -use serde::{Deserialize, Serialize}; + use std::sync::Arc; -#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct FolderPad { - pub(crate) workspaces: Vec>, - pub(crate) trash: Vec>, - #[serde(skip)] - pub(crate) delta: FolderDelta, + folder_rev: FolderRevision, + delta: FolderDelta, } impl FolderPad { pub fn new(workspaces: Vec, trash: Vec) -> CollaborateResult { - FolderPadBuilder::new() - .with_workspace(workspaces) - .with_trash(trash) - .build() + let folder_rev = FolderRevision { + workspaces: workspaces.into_iter().map(Arc::new).collect(), + trash: trash.into_iter().map(Arc::new).collect(), + }; + Self::from_folder_rev(folder_rev) + } + + pub fn from_folder_rev(folder_rev: FolderRevision) -> CollaborateResult { + let json = serde_json::to_string(&folder_rev) + .map_err(|e| CollaborateError::internal().context(format!("Serialize to folder json str failed: {}", e)))?; + let delta = TextDeltaBuilder::new().insert(&json).build(); + + Ok(Self { folder_rev, delta }) } pub fn from_revisions(revisions: Vec) -> CollaborateResult { @@ -35,7 +42,14 @@ impl FolderPad { } pub fn from_delta(delta: FolderDelta) -> CollaborateResult { - FolderPadBuilder::new().build_with_delta(delta) + // TODO: Reconvert from history if delta.to_str() failed. + let content = delta.content()?; + let folder_rev: FolderRevision = serde_json::from_str(&content).map_err(|e| { + tracing::error!("Deserialize folder from {} failed", content); + return CollaborateError::internal().context(format!("Deserialize delta to folder failed: {}", e)); + })?; + + Ok(Self { folder_rev, delta }) } pub fn delta(&self) -> &FolderDelta { @@ -44,8 +58,7 @@ impl FolderPad { pub fn reset_folder(&mut self, delta: FolderDelta) -> CollaborateResult { let folder = FolderPad::from_delta(delta)?; - self.workspaces = folder.workspaces; - self.trash = folder.trash; + self.folder_rev = folder.folder_rev; self.delta = folder.delta; Ok(self.md5()) @@ -57,13 +70,13 @@ impl FolderPad { } pub fn is_empty(&self) -> bool { - self.workspaces.is_empty() && self.trash.is_empty() + self.folder_rev.workspaces.is_empty() && self.folder_rev.trash.is_empty() } #[tracing::instrument(level = "trace", skip(self, workspace_rev), fields(workspace_name=%workspace_rev.name), err)] - pub fn create_workspace(&mut self, workspace_rev: WorkspaceRevision) -> CollaborateResult> { + pub fn create_workspace(&mut self, workspace_rev: WorkspaceRevision) -> CollaborateResult> { let workspace = Arc::new(workspace_rev); - if self.workspaces.contains(&workspace) { + if self.folder_rev.workspaces.contains(&workspace) { tracing::warn!("[RootFolder]: Duplicate workspace"); return Ok(None); } @@ -79,7 +92,7 @@ impl FolderPad { workspace_id: &str, name: Option, desc: Option, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.with_workspace(workspace_id, |workspace| { if let Some(name) = name { workspace.name = name; @@ -96,6 +109,7 @@ impl FolderPad { match workspace_id { None => { let workspaces = self + .folder_rev .workspaces .iter() .map(|workspace| workspace.as_ref().clone()) @@ -103,7 +117,12 @@ impl FolderPad { Ok(workspaces) } Some(workspace_id) => { - if let Some(workspace) = self.workspaces.iter().find(|workspace| workspace.id == workspace_id) { + if let Some(workspace) = self + .folder_rev + .workspaces + .iter() + .find(|workspace| workspace.id == workspace_id) + { Ok(vec![workspace.as_ref().clone()]) } else { Err(CollaborateError::record_not_found() @@ -114,7 +133,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), err)] - pub fn delete_workspace(&mut self, workspace_id: &str) -> CollaborateResult> { + pub fn delete_workspace(&mut self, workspace_id: &str) -> CollaborateResult> { self.modify_workspaces(|workspaces| { workspaces.retain(|w| w.id != workspace_id); Ok(Some(())) @@ -122,7 +141,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), fields(app_name=%app_rev.name), err)] - pub fn create_app(&mut self, app_rev: AppRevision) -> CollaborateResult> { + pub fn create_app(&mut self, app_rev: AppRevision) -> CollaborateResult> { let workspace_id = app_rev.workspace_id.clone(); self.with_workspace(&workspace_id, move |workspace| { if workspace.apps.contains(&app_rev) { @@ -135,7 +154,7 @@ impl FolderPad { } pub fn read_app(&self, app_id: &str) -> CollaborateResult { - for workspace in &self.workspaces { + for workspace in &self.folder_rev.workspaces { if let Some(app) = workspace.apps.iter().find(|app| app.id == app_id) { return Ok(app.clone()); } @@ -148,7 +167,7 @@ impl FolderPad { app_id: &str, name: Option, desc: Option, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.with_app(app_id, move |app| { if let Some(name) = name { app.name = name; @@ -162,7 +181,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), err)] - pub fn delete_app(&mut self, app_id: &str) -> CollaborateResult> { + pub fn delete_app(&mut self, app_id: &str) -> CollaborateResult> { let app = self.read_app(app_id)?; self.with_workspace(&app.workspace_id, |workspace| { workspace.apps.retain(|app| app.id != app_id); @@ -171,7 +190,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), err)] - pub fn move_app(&mut self, app_id: &str, from: usize, to: usize) -> CollaborateResult> { + pub fn move_app(&mut self, app_id: &str, from: usize, to: usize) -> CollaborateResult> { let app = self.read_app(app_id)?; self.with_workspace(&app.workspace_id, |workspace| { match move_vec_element(&mut workspace.apps, |app| app.id == app_id, from, to).map_err(internal_error)? { @@ -182,7 +201,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), fields(view_name=%view_rev.name), err)] - pub fn create_view(&mut self, view_rev: ViewRevision) -> CollaborateResult> { + pub fn create_view(&mut self, view_rev: ViewRevision) -> CollaborateResult> { let app_id = view_rev.belong_to_id.clone(); self.with_app(&app_id, move |app| { if app.belongings.contains(&view_rev) { @@ -195,7 +214,7 @@ impl FolderPad { } pub fn read_view(&self, view_id: &str) -> CollaborateResult { - for workspace in &self.workspaces { + for workspace in &self.folder_rev.workspaces { for app in &(*workspace.apps) { if let Some(view) = app.belongings.iter().find(|b| b.id == view_id) { return Ok(view.clone()); @@ -206,7 +225,7 @@ impl FolderPad { } pub fn read_views(&self, belong_to_id: &str) -> CollaborateResult> { - for workspace in &self.workspaces { + for workspace in &self.folder_rev.workspaces { for app in &(*workspace.apps) { if app.id == belong_to_id { return Ok(app.belongings.to_vec()); @@ -222,7 +241,7 @@ impl FolderPad { name: Option, desc: Option, modified_time: i64, - ) -> CollaborateResult> { + ) -> CollaborateResult> { let view = self.read_view(view_id)?; self.with_view(&view.belong_to_id, view_id, |view| { if let Some(name) = name { @@ -239,7 +258,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), err)] - pub fn delete_view(&mut self, view_id: &str) -> CollaborateResult> { + pub fn delete_view(&mut self, view_id: &str) -> CollaborateResult> { let view = self.read_view(view_id)?; self.with_app(&view.belong_to_id, |app| { app.belongings.retain(|view| view.id != view_id); @@ -248,7 +267,7 @@ impl FolderPad { } #[tracing::instrument(level = "trace", skip(self), err)] - pub fn move_view(&mut self, view_id: &str, from: usize, to: usize) -> CollaborateResult> { + pub fn move_view(&mut self, view_id: &str, from: usize, to: usize) -> CollaborateResult> { let view = self.read_view(view_id)?; self.with_app(&view.belong_to_id, |app| { match move_vec_element(&mut app.belongings, |view| view.id == view_id, from, to).map_err(internal_error)? { @@ -258,7 +277,7 @@ impl FolderPad { }) } - pub fn create_trash(&mut self, trash: Vec) -> CollaborateResult> { + pub fn create_trash(&mut self, trash: Vec) -> CollaborateResult> { self.with_trash(|t| { let mut new_trash = trash.into_iter().map(Arc::new).collect::>>(); t.append(&mut new_trash); @@ -270,18 +289,19 @@ impl FolderPad { pub fn read_trash(&self, trash_id: Option) -> CollaborateResult> { match trash_id { None => Ok(self + .folder_rev .trash .iter() .map(|t| t.as_ref().clone()) .collect::>()), - Some(trash_id) => match self.trash.iter().find(|t| t.id == trash_id) { + Some(trash_id) => match self.folder_rev.trash.iter().find(|t| t.id == trash_id) { Some(trash) => Ok(vec![trash.as_ref().clone()]), None => Ok(vec![]), }, } } - pub fn delete_trash(&mut self, trash_ids: Option>) -> CollaborateResult> { + pub fn delete_trash(&mut self, trash_ids: Option>) -> CollaborateResult> { match trash_ids { None => self.with_trash(|trash| { trash.clear(); @@ -299,18 +319,18 @@ impl FolderPad { } pub fn to_json(&self) -> CollaborateResult { - serde_json::to_string(self) + serde_json::to_string(&self.folder_rev) .map_err(|e| CollaborateError::internal().context(format!("serial trash to json failed: {}", e))) } } impl FolderPad { - fn modify_workspaces(&mut self, f: F) -> CollaborateResult> + fn modify_workspaces(&mut self, f: F) -> CollaborateResult> where F: FnOnce(&mut Vec>) -> CollaborateResult>, { let cloned_self = self.clone(); - match f(&mut self.workspaces)? { + match f(&mut self.folder_rev.workspaces)? { None => Ok(None), Some(_) => { let old = cloned_self.to_json()?; @@ -319,14 +339,14 @@ impl FolderPad { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; - Ok(Some(FolderChange { delta, md5: self.md5() })) + Ok(Some(FolderChangeset { delta, md5: self.md5() })) } } } } } - fn with_workspace(&mut self, workspace_id: &str, f: F) -> CollaborateResult> + fn with_workspace(&mut self, workspace_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut WorkspaceRevision) -> CollaborateResult>, { @@ -340,12 +360,12 @@ impl FolderPad { }) } - fn with_trash(&mut self, f: F) -> CollaborateResult> + fn with_trash(&mut self, f: F) -> CollaborateResult> where F: FnOnce(&mut Vec>) -> CollaborateResult>, { let cloned_self = self.clone(); - match f(&mut self.trash)? { + match f(&mut self.folder_rev.trash)? { None => Ok(None), Some(_) => { let old = cloned_self.to_json()?; @@ -354,18 +374,19 @@ impl FolderPad { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; - Ok(Some(FolderChange { delta, md5: self.md5() })) + Ok(Some(FolderChangeset { delta, md5: self.md5() })) } } } } } - fn with_app(&mut self, app_id: &str, f: F) -> CollaborateResult> + fn with_app(&mut self, app_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut AppRevision) -> CollaborateResult>, { let workspace_id = match self + .folder_rev .workspaces .iter() .find(|workspace| workspace.apps.iter().any(|app| app.id == app_id)) @@ -383,7 +404,7 @@ impl FolderPad { }) } - fn with_view(&mut self, belong_to_id: &str, view_id: &str, f: F) -> CollaborateResult> + fn with_view(&mut self, belong_to_id: &str, view_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut ViewRevision) -> CollaborateResult>, { @@ -414,14 +435,13 @@ pub fn initial_folder_delta(folder_pad: &FolderPad) -> CollaborateResult Self { FolderPad { - workspaces: vec![], - trash: vec![], + folder_rev: FolderRevision::default(), delta: default_folder_delta(), } } } -pub struct FolderChange { +pub struct FolderChangeset { pub delta: FolderDelta, /// md5: the md5 of the FolderPad's delta after applying the change. pub md5: String, @@ -433,7 +453,9 @@ mod tests { use crate::{client_folder::folder_pad::FolderPad, entities::folder::FolderDelta}; use chrono::Utc; - use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision, WorkspaceRevision}; + use flowy_folder_data_model::revision::{ + AppRevision, FolderRevision, TrashRevision, ViewRevision, WorkspaceRevision, + }; use lib_ot::core::{OperationTransform, TextDelta, TextDeltaBuilder}; #[test] @@ -747,14 +769,16 @@ mod tests { } fn test_folder() -> (FolderPad, FolderDelta, WorkspaceRevision) { - let mut folder = FolderPad::default(); - let folder_json = serde_json::to_string(&folder).unwrap(); + let folder_rev = FolderRevision::default(); + let folder_json = serde_json::to_string(&folder_rev).unwrap(); let mut delta = TextDeltaBuilder::new().insert(&folder_json).build(); let mut workspace_rev = WorkspaceRevision::default(); workspace_rev.name = "😁 my first workspace".to_owned(); workspace_rev.id = "1".to_owned(); + let mut folder = FolderPad::from_folder_rev(folder_rev).unwrap(); + delta = delta .compose(&folder.create_workspace(workspace_rev.clone()).unwrap().unwrap().delta) .unwrap(); @@ -763,16 +787,16 @@ mod tests { } fn test_app_folder() -> (FolderPad, FolderDelta, AppRevision) { - let (mut folder, mut initial_delta, workspace) = test_folder(); + let (mut folder_rev, mut initial_delta, workspace) = test_folder(); let mut app_rev = AppRevision::default(); app_rev.workspace_id = workspace.id; app_rev.name = "😁 my first app".to_owned(); initial_delta = initial_delta - .compose(&folder.create_app(app_rev.clone()).unwrap().unwrap().delta) + .compose(&folder_rev.create_app(app_rev.clone()).unwrap().unwrap().delta) .unwrap(); - (folder, initial_delta, app_rev) + (folder_rev, initial_delta, app_rev) } fn test_view_folder() -> (FolderPad, FolderDelta, ViewRevision) { @@ -789,14 +813,14 @@ mod tests { } fn test_trash() -> (FolderPad, FolderDelta, TrashRevision) { - let mut folder = FolderPad::default(); - let folder_json = serde_json::to_string(&folder).unwrap(); + let folder_rev = FolderRevision::default(); + let folder_json = serde_json::to_string(&folder_rev).unwrap(); let mut delta = TextDeltaBuilder::new().insert(&folder_json).build(); let mut trash_rev = TrashRevision::default(); trash_rev.name = "🚽 my first trash".to_owned(); trash_rev.id = "1".to_owned(); - + let mut folder = FolderPad::from_folder_rev(folder_rev).unwrap(); delta = delta .compose( &folder @@ -823,8 +847,7 @@ mod tests { let json1 = old.to_json().unwrap(); let json2 = new.to_json().unwrap(); - let expect_folder: FolderPad = serde_json::from_str(expected).unwrap(); - assert_eq!(json1, expect_folder.to_json().unwrap()); + assert_eq!(json1, expected); assert_eq!(json1, json2); } } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs deleted file mode 100644 index 3ef9251e4a..0000000000 --- a/shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs +++ /dev/null @@ -1,432 +0,0 @@ -use crate::entities::revision::{md5, RepeatedRevision, Revision}; -use crate::errors::{internal_error, CollaborateError, CollaborateResult}; -use crate::util::{cal_diff, make_delta_from_revisions}; -use bytes::Bytes; -use flowy_grid_data_model::entities::{ - gen_block_id, gen_grid_id, FieldChangesetParams, FieldMeta, FieldOrder, FieldType, GridBlockInfoChangeset, - GridBlockMetaSnapshot, GridMeta, -}; -use lib_infra::util::move_vec_element; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; -use std::collections::HashMap; -use std::sync::Arc; - -pub type GridMetaDelta = PlainTextDelta; -pub type GridDeltaBuilder = PlainTextDeltaBuilder; - -pub struct GridMetaPad { - pub(crate) grid_meta: Arc, - pub(crate) delta: GridMetaDelta, -} - -pub trait JsonDeserializer { - fn deserialize(&self, type_option_data: Vec) -> CollaborateResult; -} - -impl GridMetaPad { - pub async fn duplicate_grid_meta(&self) -> (Vec, Vec) { - let fields = self.grid_meta.fields.to_vec(); - - let blocks = self - .grid_meta - .blocks - .iter() - .map(|block| { - let mut duplicated_block = block.clone(); - duplicated_block.block_id = gen_block_id(); - duplicated_block - }) - .collect::>(); - - (fields, blocks) - } - - pub fn from_delta(delta: GridMetaDelta) -> CollaborateResult { - let s = delta.to_str()?; - let grid: GridMeta = serde_json::from_str(&s) - .map_err(|e| CollaborateError::internal().context(format!("Deserialize delta to grid failed: {}", e)))?; - - Ok(Self { - grid_meta: Arc::new(grid), - delta, - }) - } - - pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { - let grid_delta: GridMetaDelta = make_delta_from_revisions::(revisions)?; - Self::from_delta(grid_delta) - } - - #[tracing::instrument(level = "debug", skip_all, err)] - pub fn create_field_meta( - &mut self, - new_field_meta: FieldMeta, - start_field_id: Option, - ) -> CollaborateResult> { - self.modify_grid(|grid_meta| { - // Check if the field exists or not - if grid_meta - .fields - .iter() - .any(|field_meta| field_meta.id == new_field_meta.id) - { - tracing::error!("Duplicate grid field"); - return Ok(None); - } - - let insert_index = match start_field_id { - None => None, - Some(start_field_id) => grid_meta.fields.iter().position(|field| field.id == start_field_id), - }; - - match insert_index { - None => grid_meta.fields.push(new_field_meta), - Some(index) => grid_meta.fields.insert(index, new_field_meta), - } - Ok(Some(())) - }) - } - - pub fn delete_field_meta(&mut self, field_id: &str) -> CollaborateResult> { - self.modify_grid( - |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { - None => Ok(None), - Some(index) => { - grid_meta.fields.remove(index); - Ok(Some(())) - } - }, - ) - } - - pub fn duplicate_field_meta( - &mut self, - field_id: &str, - duplicated_field_id: &str, - ) -> CollaborateResult> { - self.modify_grid( - |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { - None => Ok(None), - Some(index) => { - let mut duplicate_field_meta = grid_meta.fields[index].clone(); - duplicate_field_meta.id = duplicated_field_id.to_string(); - duplicate_field_meta.name = format!("{} (copy)", duplicate_field_meta.name); - grid_meta.fields.insert(index + 1, duplicate_field_meta); - Ok(Some(())) - } - }, - ) - } - - pub fn switch_to_field( - &mut self, - field_id: &str, - field_type: FieldType, - type_option_json_builder: B, - ) -> CollaborateResult> - where - B: FnOnce(&FieldType) -> String, - { - self.modify_grid(|grid_meta| { - // - match grid_meta.fields.iter_mut().find(|field_meta| field_meta.id == field_id) { - None => { - tracing::warn!("Can not find the field with id: {}", field_id); - Ok(None) - } - Some(field_meta) => { - if field_meta.get_type_option_str(&field_type).is_none() { - let type_option_json = type_option_json_builder(&field_type); - field_meta.insert_type_option_str(&field_type, type_option_json); - } - - field_meta.field_type = field_type; - Ok(Some(())) - } - } - }) - } - - pub fn update_field_meta( - &mut self, - changeset: FieldChangesetParams, - deserializer: T, - ) -> CollaborateResult> { - let field_id = changeset.field_id.clone(); - self.modify_field(&field_id, |field| { - let mut is_changed = None; - if let Some(name) = changeset.name { - field.name = name; - is_changed = Some(()) - } - - if let Some(desc) = changeset.desc { - field.desc = desc; - is_changed = Some(()) - } - - if let Some(field_type) = changeset.field_type { - field.field_type = field_type; - is_changed = Some(()) - } - - if let Some(frozen) = changeset.frozen { - field.frozen = frozen; - is_changed = Some(()) - } - - if let Some(visibility) = changeset.visibility { - field.visibility = visibility; - is_changed = Some(()) - } - - if let Some(width) = changeset.width { - field.width = width; - is_changed = Some(()) - } - - if let Some(type_option_data) = changeset.type_option_data { - match deserializer.deserialize(type_option_data) { - Ok(json_str) => { - let field_type = field.field_type.clone(); - field.insert_type_option_str(&field_type, json_str); - is_changed = Some(()) - } - Err(err) => { - tracing::error!("Deserialize data to type option json failed: {}", err); - } - } - } - - Ok(is_changed) - }) - } - - pub fn get_field_meta(&self, field_id: &str) -> Option<(usize, &FieldMeta)> { - self.grid_meta - .fields - .iter() - .enumerate() - .find(|(_, field)| field.id == field_id) - } - - pub fn replace_field_meta(&mut self, field_meta: FieldMeta) -> CollaborateResult> { - self.modify_grid( - |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_meta.id) { - None => Ok(None), - Some(index) => { - grid_meta.fields.remove(index); - grid_meta.fields.insert(index, field_meta); - Ok(Some(())) - } - }, - ) - } - - pub fn move_field( - &mut self, - field_id: &str, - from_index: usize, - to_index: usize, - ) -> CollaborateResult> { - self.modify_grid(|grid_meta| { - match move_vec_element( - &mut grid_meta.fields, - |field| field.id == field_id, - from_index, - to_index, - ) - .map_err(internal_error)? - { - true => Ok(Some(())), - false => Ok(None), - } - }) - } - - pub fn contain_field(&self, field_id: &str) -> bool { - self.grid_meta.fields.iter().any(|field| field.id == field_id) - } - - pub fn get_field_orders(&self) -> Vec { - self.grid_meta.fields.iter().map(FieldOrder::from).collect() - } - - pub fn get_field_metas(&self, field_orders: Option>) -> CollaborateResult> { - match field_orders { - None => Ok(self.grid_meta.fields.clone()), - Some(field_orders) => { - let field_by_field_id = self - .grid_meta - .fields - .iter() - .map(|field| (&field.id, field)) - .collect::>(); - - let fields = field_orders - .iter() - .flat_map(|field_order| match field_by_field_id.get(&field_order.field_id) { - None => { - tracing::error!("Can't find the field with id: {}", field_order.field_id); - None - } - Some(field) => Some((*field).clone()), - }) - .collect::>(); - Ok(fields) - } - } - } - - pub fn create_block_meta(&mut self, block: GridBlockMetaSnapshot) -> CollaborateResult> { - self.modify_grid(|grid_meta| { - if grid_meta.blocks.iter().any(|b| b.block_id == block.block_id) { - tracing::warn!("Duplicate grid block"); - Ok(None) - } else { - match grid_meta.blocks.last() { - None => grid_meta.blocks.push(block), - Some(last_block) => { - if last_block.start_row_index > block.start_row_index - && last_block.len() > block.start_row_index - { - let msg = "GridBlock's start_row_index should be greater than the last_block's start_row_index and its len".to_string(); - return Err(CollaborateError::internal().context(msg)) - } - grid_meta.blocks.push(block); - } - } - Ok(Some(())) - } - }) - } - - pub fn get_block_metas(&self) -> Vec { - self.grid_meta.blocks.clone() - } - - pub fn update_block_meta(&mut self, changeset: GridBlockInfoChangeset) -> CollaborateResult> { - let block_id = changeset.block_id.clone(); - self.modify_block(&block_id, |block| { - let mut is_changed = None; - - if let Some(row_count) = changeset.row_count { - block.row_count = row_count; - is_changed = Some(()); - } - - if let Some(start_row_index) = changeset.start_row_index { - block.start_row_index = start_row_index; - is_changed = Some(()); - } - - Ok(is_changed) - }) - } - - pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) - } - - pub fn delta_str(&self) -> String { - self.delta.to_delta_str() - } - - pub fn delta_bytes(&self) -> Bytes { - self.delta.to_delta_bytes() - } - - pub fn fields(&self) -> &[FieldMeta] { - &self.grid_meta.fields - } - - fn modify_grid(&mut self, f: F) -> CollaborateResult> - where - F: FnOnce(&mut GridMeta) -> CollaborateResult>, - { - let cloned_grid = self.grid_meta.clone(); - match f(Arc::make_mut(&mut self.grid_meta))? { - None => Ok(None), - Some(_) => { - let old = json_from_grid(&cloned_grid)?; - let new = json_from_grid(&self.grid_meta)?; - match cal_diff::(old, new) { - None => Ok(None), - Some(delta) => { - self.delta = self.delta.compose(&delta)?; - Ok(Some(GridChangeset { delta, md5: self.md5() })) - } - } - } - } - } - - pub fn modify_block(&mut self, block_id: &str, f: F) -> CollaborateResult> - where - F: FnOnce(&mut GridBlockMetaSnapshot) -> CollaborateResult>, - { - self.modify_grid( - |grid_meta| match grid_meta.blocks.iter().position(|block| block.block_id == block_id) { - None => { - tracing::warn!("[GridMetaPad]: Can't find any block with id: {}", block_id); - Ok(None) - } - Some(index) => f(&mut grid_meta.blocks[index]), - }, - ) - } - - pub fn modify_field(&mut self, field_id: &str, f: F) -> CollaborateResult> - where - F: FnOnce(&mut FieldMeta) -> CollaborateResult>, - { - self.modify_grid( - |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { - None => { - tracing::warn!("[GridMetaPad]: Can't find any field with id: {}", field_id); - Ok(None) - } - Some(index) => f(&mut grid_meta.fields[index]), - }, - ) - } -} - -fn json_from_grid(grid: &Arc) -> CollaborateResult { - let json = serde_json::to_string(grid) - .map_err(|err| internal_error(format!("Serialize grid to json str failed. {:?}", err)))?; - Ok(json) -} - -pub struct GridChangeset { - pub delta: GridMetaDelta, - /// md5: the md5 of the grid after applying the change. - pub md5: String, -} - -pub fn make_grid_delta(grid_meta: &GridMeta) -> GridMetaDelta { - let json = serde_json::to_string(&grid_meta).unwrap(); - PlainTextDeltaBuilder::new().insert(&json).build() -} - -pub fn make_grid_revisions(user_id: &str, grid_meta: &GridMeta) -> RepeatedRevision { - let delta = make_grid_delta(grid_meta); - let bytes = delta.to_delta_bytes(); - let revision = Revision::initial_revision(user_id, &grid_meta.grid_id, bytes); - revision.into() -} - -impl std::default::Default for GridMetaPad { - fn default() -> Self { - let grid = GridMeta { - grid_id: gen_grid_id(), - fields: vec![], - blocks: vec![], - }; - let delta = make_grid_delta(&grid); - GridMetaPad { - grid_meta: Arc::new(grid), - delta, - } - } -} diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index b0188066ef..ffe1e81e9f 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -53,8 +53,11 @@ impl GridRevisionPad { pub fn from_delta(delta: GridRevisionDelta) -> CollaborateResult { let content = delta.content()?; - let grid: GridRevision = serde_json::from_str(&content) - .map_err(|e| CollaborateError::internal().context(format!("Deserialize delta to grid failed: {}", e)))?; + let grid: GridRevision = serde_json::from_str(&content).map_err(|e| { + let msg = format!("Deserialize delta to grid failed: {}", e); + tracing::error!("{}", msg); + CollaborateError::internal().context(msg) + })?; Ok(Self { grid_rev: Arc::new(grid), diff --git a/shared-lib/lib-ot/src/core/operation/builder.rs b/shared-lib/lib-ot/src/core/operation/builder.rs index 9483d4cae7..c4f9cc8277 100644 --- a/shared-lib/lib-ot/src/core/operation/builder.rs +++ b/shared-lib/lib-ot/src/core/operation/builder.rs @@ -4,6 +4,7 @@ use crate::rich_text::RichTextAttributes; pub type RichTextOpBuilder = OperationsBuilder; pub type PlainTextOpBuilder = OperationsBuilder; +#[derive(Default)] pub struct OperationsBuilder { operations: Vec>, } @@ -13,17 +14,17 @@ where T: Attributes, { pub fn new() -> OperationsBuilder { - OperationsBuilder { operations: vec![] } + OperationsBuilder::default() } pub fn retain_with_attributes(mut self, n: usize, attributes: T) -> OperationsBuilder { - let retain = Operation::retain_with_attributes(n.into(), attributes); + let retain = Operation::retain_with_attributes(n, attributes); self.operations.push(retain); self } pub fn retain(mut self, n: usize) -> OperationsBuilder { - let retain = Operation::retain(n.into()); + let retain = Operation::retain(n); self.operations.push(retain); self } @@ -34,13 +35,13 @@ where } pub fn insert_with_attributes(mut self, s: &str, attributes: T) -> OperationsBuilder { - let insert = Operation::insert_with_attributes(s.into(), attributes); + let insert = Operation::insert_with_attributes(s, attributes); self.operations.push(insert); self } pub fn insert(mut self, s: &str) -> OperationsBuilder { - let insert = Operation::insert(s.into()); + let insert = Operation::insert(s); self.operations.push(insert); self } From ba3e8cc14b7295ebe5b1b0c6fd4686e5ba658e99 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 10:35:00 +0800 Subject: [PATCH 029/224] chore: format json output --- shared-lib/flowy-folder-data-model/src/revision/trash_rev.rs | 2 +- shared-lib/flowy-sync/src/client_folder/folder_pad.rs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/shared-lib/flowy-folder-data-model/src/revision/trash_rev.rs b/shared-lib/flowy-folder-data-model/src/revision/trash_rev.rs index c72fc61dad..0855fb3d29 100644 --- a/shared-lib/flowy-folder-data-model/src/revision/trash_rev.rs +++ b/shared-lib/flowy-folder-data-model/src/revision/trash_rev.rs @@ -33,7 +33,7 @@ impl<'de> serde::Deserialize<'de> for TrashTypeRevision { type Value = TrashTypeRevision; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("u8") + formatter.write_str("expected enum TrashTypeRevision with type: u8") } fn visit_i8(self, v: i8) -> Result diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index 278c0970ff..28c8e087a1 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -457,6 +457,7 @@ mod tests { AppRevision, FolderRevision, TrashRevision, ViewRevision, WorkspaceRevision, }; use lib_ot::core::{OperationTransform, TextDelta, TextDeltaBuilder}; + use serde_json::json; #[test] fn folder_add_workspace() { @@ -847,6 +848,10 @@ mod tests { let json1 = old.to_json().unwrap(); let json2 = new.to_json().unwrap(); + // format the json str + let folder_rev: FolderRevision = serde_json::from_str(expected).unwrap(); + let expected = serde_json::to_string(&folder_rev).unwrap(); + assert_eq!(json1, expected); assert_eq!(json1, json2); } From d40a3c33fd11644199479883c2c84432056f07ff Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 11:28:56 +0800 Subject: [PATCH 030/224] feat: copy paste check box --- .../lib/infra/html_converter.dart | 26 ++++++++++++------- .../copy_paste_handler.dart | 17 +++--------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 708e47cb85..13f30ce4ef 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; @@ -206,18 +207,29 @@ class HTMLConverter { } } -html.Element deltaToHtml(Delta delta, [String? subType]) { +html.Element textNodeToHtml(TextNode textNode, {int? end}) { + String? subType = textNode.attributes["subtype"]; + return deltaToHtml(textNode.delta, subType: subType, end: end); +} + +html.Element deltaToHtml(Delta delta, {String? subType, int? end}) { + if (end != null) { + delta = delta.slice(0, end); + } + final childNodes = []; String tagName = tagParagraph; - if (subType == "bulleted-list") { + if (subType == StyleKey.bulletedList) { tagName = tagList; + } else if (subType == StyleKey.checkbox) { + childNodes.add(html.Element.html('')); } for (final op in delta.operations) { if (op is TextInsert) { final attributes = op.attributes; - if (attributes != null && attributes["bold"] == true) { + if (attributes != null && attributes[StyleKey.bold] == true) { final strong = html.Element.tag("strong"); strong.append(html.Text(op.content)); childNodes.add(strong); @@ -246,13 +258,7 @@ html.Element deltaToHtml(Delta delta, [String? subType]) { String stringify(html.Node node) { if (node is html.Element) { - String result = '<${node.localName}>'; - - for (final node in node.nodes) { - result += stringify(node); - } - - return result += ''; + return node.outerHtml; } if (node is html.Text) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index d3eeb073b9..3d1979dad0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -60,10 +60,7 @@ _handleCopy(EditorState editorState) async { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { final textNode = nodeAtPath as TextNode; - final delta = - textNode.delta.slice(selection.start.offset, selection.end.offset); - - final htmlString = stringify(deltaToHtml(delta)); + final htmlString = stringify(textNodeToHtml(textNode)); debugPrint('copy html: $htmlString'); RichClipboard.setData(RichClipboardData(html: htmlString)); } else { @@ -81,18 +78,12 @@ _handleCopy(EditorState editorState) async { final node = traverser.current; if (node.type == "text") { final textNode = node as TextNode; - String? subType = textNode.attributes["subtype"]; if (node == beginNode) { - final htmlElement = - deltaToHtml(textNode.delta.slice(selection.start.offset), subType); - nodes.add(htmlElement); + nodes.add(textNodeToHtml(textNode)); } else if (node == endNode) { - final htmlElement = - deltaToHtml(textNode.delta.slice(0, selection.end.offset), subType); - nodes.add(htmlElement); + nodes.add(textNodeToHtml(textNode, end: selection.end.offset)); } else { - final htmlElement = deltaToHtml(textNode.delta, subType); - nodes.add(htmlElement); + nodes.add(textNodeToHtml(textNode)); } } // TODO: handle image and other blocks From a59c809847c8737ed1592d4b9bb2a913c72e870c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 11:44:32 +0800 Subject: [PATCH 031/224] feat: copy checkbox style --- .../lib/infra/html_converter.dart | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 13f30ce4ef..68bb8555f9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -149,8 +149,16 @@ class HTMLConverter { _handleImage(nodes, image); return; } + final testInput = element.querySelector("input"); + bool checked = false; + final isCheckbox = + testInput != null && testInput.attributes["type"] == "checkbox"; + if (isCheckbox) { + checked = testInput.attributes.containsKey("checked") && + testInput.attributes["checked"] != "false"; + } - var delta = Delta(); + final delta = Delta(); for (final child in element.nodes.toList()) { if (child is html.Element) { @@ -161,7 +169,12 @@ class HTMLConverter { } if (delta.operations.isNotEmpty) { - nodes.add(TextNode(type: "text", delta: delta, attributes: attributes)); + final textNode = TextNode(type: "text", delta: delta); + if (isCheckbox) { + textNode.attributes["subtype"] = StyleKey.checkbox; + textNode.attributes["checkbox"] = checked; + } + nodes.add(textNode); } } @@ -207,12 +220,16 @@ class HTMLConverter { } } -html.Element textNodeToHtml(TextNode textNode, {int? end}) { +html.Element textNodeToHtml(TextNode textNode, {int? end, bool? checked}) { String? subType = textNode.attributes["subtype"]; - return deltaToHtml(textNode.delta, subType: subType, end: end); + return deltaToHtml(textNode.delta, + subType: subType, + end: end, + checked: textNode.attributes["checkbox"] == true); } -html.Element deltaToHtml(Delta delta, {String? subType, int? end}) { +html.Element deltaToHtml(Delta delta, + {String? subType, int? end, bool? checked}) { if (end != null) { delta = delta.slice(0, end); } @@ -223,7 +240,11 @@ html.Element deltaToHtml(Delta delta, {String? subType, int? end}) { if (subType == StyleKey.bulletedList) { tagName = tagList; } else if (subType == StyleKey.checkbox) { - childNodes.add(html.Element.html('')); + final node = html.Element.html(''); + if (checked != null && checked) { + node.attributes["checked"] = "true"; + } + childNodes.add(node); } for (final op in delta.operations) { From 76c3e440e72d1954d3dbe32fff96bb07a791d6e2 Mon Sep 17 00:00:00 2001 From: Victor Teles Date: Wed, 10 Aug 2022 02:17:27 -0300 Subject: [PATCH 032/224] fix: fixed controls space --- .../presentation/home/home_layout.dart | 2 ++ .../presentation/home/home_screen.dart | 4 ++- .../presentation/home/home_stack.dart | 25 ++++++++----------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart index dde5091d44..94cb1874cd 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart @@ -15,6 +15,7 @@ class HomeLayout { late double editPanelWidth; late double homePageLOffset; late double homePageROffset; + late double menuSpacing; late Duration animDuration; HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint, @@ -37,6 +38,7 @@ class HomeLayout { } homePageLOffset = showMenu ? menuWidth : 0.0; + menuSpacing = showMenu ? 0 : 80.0; animDuration = .35.seconds; editPanelWidth = HomeSizes.editPanelWidth; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart index e38a2f4614..3259e3d843 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart @@ -87,7 +87,9 @@ class _HomeScreenState extends State { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final layout = HomeLayout(context, constraints, state.forceCollapse); - const homeStack = HomeStack(); + final homeStack = HomeStack( + layout: layout, + ); final menu = _buildHomeMenu( layout: layout, context: context, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart index d58f849cd7..a64fc8b5b5 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart @@ -17,11 +17,14 @@ import 'package:app_flowy/core/frameless_window.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/style_widget/extension.dart'; import 'package:flowy_infra/notifier.dart'; +import 'home_layout.dart'; typedef NavigationCallback = void Function(String id); class HomeStack extends StatelessWidget { - const HomeStack({Key? key}) : super(key: key); + const HomeStack({Key? key, required this.layout}) : super(key: key); + + final HomeLayout layout; @override Widget build(BuildContext context) { @@ -30,7 +33,7 @@ class HomeStack extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - getIt().stackTopBar(), + getIt().stackTopBar(layout: layout), Expanded( child: Container( color: theme.surface, @@ -143,7 +146,7 @@ class HomeStackManager { void setStackWithId(String id) {} - Widget stackTopBar() { + Widget stackTopBar({required HomeLayout layout}) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: _notifier), @@ -151,7 +154,7 @@ class HomeStackManager { child: Selector( selector: (context, notifier) => notifier.titleWidget, builder: (context, widget, child) { - return const MoveWindowDetector(child: HomeTopBar()); + return MoveWindowDetector(child: HomeTopBar(layout: layout)); }, ), ); @@ -181,7 +184,9 @@ class HomeStackManager { } class HomeTopBar extends StatelessWidget { - const HomeTopBar({Key? key}) : super(key: key); + const HomeTopBar({Key? key, required this.layout}) : super(key: key); + + final HomeLayout layout; @override Widget build(BuildContext context) { @@ -192,15 +197,7 @@ class HomeTopBar extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - BlocBuilder( - buildWhen: ((previous, current) => - previous.isMenuCollapsed != current.isMenuCollapsed), - builder: (context, state) { - if (state.isMenuCollapsed && Platform.isMacOS) { - return const HSpace(80); - } - return const HSpace(0); - }), + HSpace(layout.menuSpacing), const FlowyNavigation(), const HSpace(16), ChangeNotifierProvider.value( From 4140994fedb603c4e1a63aad6ea0af9e274a9167 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 11:52:04 +0800 Subject: [PATCH 033/224] feat: paste ordered list --- .../lib/infra/html_converter.dart | 301 +++++++++++------- .../copy_paste_handler.dart | 70 +--- 2 files changed, 198 insertions(+), 173 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 68bb8555f9..bf743fb497 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -4,14 +4,13 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; const String tagH1 = "h1"; const String tagH2 = "h2"; const String tagH3 = "h3"; +const String tagOrderedList = "ol"; const String tagUnorderedList = "ul"; const String tagList = "li"; const String tagParagraph = "p"; @@ -22,23 +21,21 @@ const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; -class HTMLConverter { +/// Converting the HTML to nodes +class HTMLToNodesConverter { final html.Document _document; bool _inParagraph = false; - HTMLConverter(String htmlString) : _document = parse(htmlString); + HTMLToNodesConverter(String htmlString) : _document = parse(htmlString); List toNodes() { - final result = []; - final childNodes = _document.body?.nodes.toList() ?? []; - _handleContainer(result, childNodes); - - return result; + return _handleContainer(childNodes); } - _handleContainer(List nodes, List childNodes) { + List _handleContainer(List childNodes) { final delta = Delta(); + final result = []; for (final child in childNodes) { if (child is html.Element) { if (child.localName == tagAnchor || @@ -50,55 +47,60 @@ class HTMLConverter { // Google docs wraps the the content inside the tag. // It's strange if (!_inParagraph) { - _handleBTag(nodes, child); + result.addAll(_handleBTag(child)); } else { - _handleRichText(nodes, child); + result.add(_handleRichText(child)); } } else { - _handleElement(nodes, child); + result.addAll(_handleElement(child)); } } else { delta.insert(child.text ?? ""); } } if (delta.operations.isNotEmpty) { - nodes.add(TextNode(type: "text", delta: delta)); + result.add(TextNode(type: "text", delta: delta)); } + return result; } - _handleBTag(List nodes, html.Element element) { + List _handleBTag(html.Element element) { final childNodes = element.nodes; - _handleContainer(nodes, childNodes); + return _handleContainer(childNodes); } - _handleElement(List nodes, html.Element element, + List _handleElement(html.Element element, [Map? attributes]) { if (element.localName == tagH1) { - _handleHeadingElement(nodes, element, tagH1); + return [_handleHeadingElement(element, tagH1)]; } else if (element.localName == tagH2) { - _handleHeadingElement(nodes, element, tagH2); + return [_handleHeadingElement(element, tagH2)]; } else if (element.localName == tagH3) { - _handleHeadingElement(nodes, element, tagH3); + return [_handleHeadingElement(element, tagH3)]; } else if (element.localName == tagUnorderedList) { - _handleUnorderedList(nodes, element); + return _handleUnorderedList(element); + } else if (element.localName == tagOrderedList) { + return _handleOrderedList(element); } else if (element.localName == tagList) { - _handleListElement(nodes, element); + return _handleListElement(element); } else if (element.localName == tagParagraph) { - _handleParagraph(nodes, element, attributes); + return [_handleParagraph(element, attributes)]; } else { final delta = Delta(); delta.insert(element.text); if (delta.operations.isNotEmpty) { - nodes.add(TextNode(type: "text", delta: delta)); + return [TextNode(type: "text", delta: delta)]; } } + return []; } - _handleParagraph(List nodes, html.Element element, + Node _handleParagraph(html.Element element, [Map? attributes]) { _inParagraph = true; - _handleRichText(nodes, element, attributes); + final node = _handleRichText(element, attributes); _inParagraph = false; + return node; } Attributes? _getDeltaAttributesFromHtmlAttributes( @@ -142,12 +144,12 @@ class HTMLConverter { } } - _handleRichText(List nodes, html.Element element, + Node _handleRichText(html.Element element, [Map? attributes]) { final image = element.querySelector(tagImage); if (image != null) { - _handleImage(nodes, image); - return; + final imageNode = _handleImage(image); + return imageNode; } final testInput = element.querySelector("input"); bool checked = false; @@ -168,112 +170,197 @@ class HTMLConverter { } } - if (delta.operations.isNotEmpty) { - final textNode = TextNode(type: "text", delta: delta); - if (isCheckbox) { - textNode.attributes["subtype"] = StyleKey.checkbox; - textNode.attributes["checkbox"] = checked; - } - nodes.add(textNode); + final textNode = TextNode(type: "text", delta: delta); + if (isCheckbox) { + textNode.attributes["subtype"] = StyleKey.checkbox; + textNode.attributes["checkbox"] = checked; } + return textNode; } - _handleImage(List nodes, html.Element element) { + Node _handleImage(html.Element element) { final src = element.attributes["src"]; final attributes = {}; if (src != null) { attributes["image_src"] = src; } - debugPrint("insert image: $src"); - nodes.add( - Node(type: "image", attributes: attributes, children: LinkedList())); + return Node(type: "image", attributes: attributes, children: LinkedList()); } - _handleUnorderedList(List nodes, html.Element element) { + List _handleUnorderedList(html.Element element) { + final result = []; element.children.forEach((child) { - _handleListElement(nodes, child); + result.addAll( + _handleListElement(child, {"subtype": StyleKey.bulletedList})); }); + return result; } - _handleHeadingElement( - List nodes, + List _handleOrderedList(html.Element element) { + final result = []; + element.children.forEach((child) { + result + .addAll(_handleListElement(child, {"subtype": StyleKey.numberList})); + }); + return result; + } + + Node _handleHeadingElement( html.Element element, String headingStyle, ) { final delta = Delta(); delta.insert(element.text); - if (delta.operations.isNotEmpty) { - nodes.add(TextNode( - type: "text", - attributes: {"subtype": "heading", "heading": headingStyle}, - delta: delta)); - } + return TextNode( + type: "text", + attributes: {"subtype": "heading", "heading": headingStyle}, + delta: delta); } - _handleListElement(List nodes, html.Element element) { + List _handleListElement(html.Element element, + [Map? attributes]) { + final result = []; final childNodes = element.nodes.toList(); for (final child in childNodes) { if (child is html.Element) { - _handleElement(nodes, child, {"subtype": "bulleted-list"}); + result.addAll(_handleElement(child, attributes)); } } - } -} - -html.Element textNodeToHtml(TextNode textNode, {int? end, bool? checked}) { - String? subType = textNode.attributes["subtype"]; - return deltaToHtml(textNode.delta, - subType: subType, - end: end, - checked: textNode.attributes["checkbox"] == true); -} - -html.Element deltaToHtml(Delta delta, - {String? subType, int? end, bool? checked}) { - if (end != null) { - delta = delta.slice(0, end); - } - - final childNodes = []; - String tagName = tagParagraph; - - if (subType == StyleKey.bulletedList) { - tagName = tagList; - } else if (subType == StyleKey.checkbox) { - final node = html.Element.html(''); - if (checked != null && checked) { - node.attributes["checked"] = "true"; - } - childNodes.add(node); - } - - for (final op in delta.operations) { - if (op is TextInsert) { - final attributes = op.attributes; - if (attributes != null && attributes[StyleKey.bold] == true) { - final strong = html.Element.tag("strong"); - strong.append(html.Text(op.content)); - childNodes.add(strong); - } else { - childNodes.add(html.Text(op.content)); - } - } - } - - if (tagName != tagParagraph) { - final p = html.Element.tag(tagParagraph); - for (final node in childNodes) { - p.append(node); - } - final result = html.Element.tag("li"); - result.append(p); return result; - } else { - final p = html.Element.tag(tagName); - for (final node in childNodes) { - p.append(node); + } +} + +class _HTMLNormalizer { + final List nodes; + html.Element? _pendingList; + + _HTMLNormalizer(this.nodes); + + List normalize() { + final result = []; + + for (final item in nodes) { + if (item is html.Text) { + result.add(item); + continue; + } + + if (item is html.Element) { + if (item.localName == "li") { + if (_pendingList != null) { + _pendingList!.append(item); + } else { + final ulItem = html.Element.tag("ul"); + ulItem.append(item); + + _pendingList = ulItem; + } + } else { + _pushList(result); + result.add(item); + } + } + } + + return result; + } + + _pushList(List result) { + if (_pendingList == null) { + return; + } + result.add(_pendingList!); + _pendingList = null; + } +} + +class NodesToHTMLConverter { + final List nodes; + final int? startOffset; + final int? endOffset; + + NodesToHTMLConverter({required this.nodes, this.startOffset, this.endOffset}); + + List toHTMLNodes() { + final result = []; + for (final node in nodes) { + if (node.type == "text") { + final textNode = node as TextNode; + if (node == nodes.first) { + result.add(_textNodeToHtml(textNode)); + } else if (node == nodes.last) { + result.add(_textNodeToHtml(textNode, end: endOffset)); + } else { + result.add(_textNodeToHtml(textNode)); + } + } + // TODO: handle image and other blocks + } + return result; + } + + String toHTMLString() { + final elements = toHTMLNodes(); + final copyString = _HTMLNormalizer(elements).normalize().fold( + "", ((previousValue, element) => previousValue + stringify(element))); + return copyString; + } + + html.Element _textNodeToHtml(TextNode textNode, {int? end}) { + String? subType = textNode.attributes["subtype"]; + return _deltaToHtml(textNode.delta, + subType: subType, + end: end, + checked: textNode.attributes["checkbox"] == true); + } + + html.Element _deltaToHtml(Delta delta, + {String? subType, int? end, bool? checked}) { + if (end != null) { + delta = delta.slice(0, end); + } + + final childNodes = []; + String tagName = tagParagraph; + + if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) { + tagName = tagList; + } else if (subType == StyleKey.checkbox) { + final node = html.Element.html(''); + if (checked != null && checked) { + node.attributes["checked"] = "true"; + } + childNodes.add(node); + } + + for (final op in delta.operations) { + if (op is TextInsert) { + final attributes = op.attributes; + if (attributes != null && attributes[StyleKey.bold] == true) { + final strong = html.Element.tag("strong"); + strong.append(html.Text(op.content)); + childNodes.add(strong); + } else { + childNodes.add(html.Text(op.content)); + } + } + } + + if (tagName != tagParagraph) { + final p = html.Element.tag(tagParagraph); + for (final node in childNodes) { + p.append(node); + } + final result = html.Element.tag(tagList); + result.append(p); + return result; + } else { + final p = html.Element.tag(tagName); + for (final node in childNodes) { + p.append(node); + } + return p; } - return p; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index 3d1979dad0..4174e8eaee 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,4 +1,3 @@ -import 'package:html/dom.dart' as html; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/infra/html_converter.dart'; @@ -7,50 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; -class _HTMLNormalizer { - final List nodes; - html.Element? _pendingList; - - _HTMLNormalizer(this.nodes); - - List normalize() { - final result = []; - - for (final item in nodes) { - if (item is Text) { - result.add(item); - continue; - } - - if (item is html.Element) { - if (item.localName == "li") { - if (_pendingList != null) { - _pendingList!.append(item); - } else { - final ulItem = html.Element.tag("ul"); - ulItem.append(item); - - _pendingList = ulItem; - } - } else { - _pushList(result); - result.add(item); - } - } - } - - return result; - } - - _pushList(List result) { - if (_pendingList == null) { - return; - } - result.add(_pendingList!); - _pendingList = null; - } -} - _handleCopy(EditorState editorState) async { final selection = editorState.cursorSelection; if (selection == null || selection.isCollapsed) { @@ -60,7 +15,7 @@ _handleCopy(EditorState editorState) async { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { final textNode = nodeAtPath as TextNode; - final htmlString = stringify(textNodeToHtml(textNode)); + final htmlString = NodesToHTMLConverter(nodes: [textNode]).toHTMLString(); debugPrint('copy html: $htmlString'); RichClipboard.setData(RichClipboardData(html: htmlString)); } else { @@ -71,26 +26,10 @@ _handleCopy(EditorState editorState) async { final beginNode = editorState.document.nodeAtPath(selection.start.path)!; final endNode = editorState.document.nodeAtPath(selection.end.path)!; - final traverser = NodeIterator(editorState.document, beginNode, endNode); - final nodes = []; - while (traverser.moveNext()) { - final node = traverser.current; - if (node.type == "text") { - final textNode = node as TextNode; - if (node == beginNode) { - nodes.add(textNodeToHtml(textNode)); - } else if (node == endNode) { - nodes.add(textNodeToHtml(textNode, end: selection.end.offset)); - } else { - nodes.add(textNodeToHtml(textNode)); - } - } - // TODO: handle image and other blocks - } + final nodes = NodeIterator(editorState.document, beginNode, endNode).toList(); - final copyString = _HTMLNormalizer(nodes).normalize().fold( - "", ((previousValue, element) => previousValue + stringify(element))); + final copyString = NodesToHTMLConverter(nodes: nodes).toHTMLString(); debugPrint('copy html: $copyString'); RichClipboard.setData(RichClipboardData(html: copyString)); } @@ -107,8 +46,7 @@ _pasteHTML(EditorState editorState, String html) { } debugPrint('paste html: $html'); - final converter = HTMLConverter(html); - final nodes = converter.toNodes(); + final nodes = HTMLToNodesConverter(html).toNodes(); if (nodes.isEmpty) { return; From e60630663568f34c2b694c2d980b3a2781c8df8b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 13:32:27 +0800 Subject: [PATCH 034/224] feat: converter --- .../lib/infra/html_converter.dart | 75 +++++++------------ 1 file changed, 25 insertions(+), 50 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index bf743fb497..a297a12db1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -230,78 +230,53 @@ class HTMLToNodesConverter { } } -class _HTMLNormalizer { - final List nodes; - html.Element? _pendingList; - - _HTMLNormalizer(this.nodes); - - List normalize() { - final result = []; - - for (final item in nodes) { - if (item is html.Text) { - result.add(item); - continue; - } - - if (item is html.Element) { - if (item.localName == "li") { - if (_pendingList != null) { - _pendingList!.append(item); - } else { - final ulItem = html.Element.tag("ul"); - ulItem.append(item); - - _pendingList = ulItem; - } - } else { - _pushList(result); - result.add(item); - } - } - } - - return result; - } - - _pushList(List result) { - if (_pendingList == null) { - return; - } - result.add(_pendingList!); - _pendingList = null; - } -} - class NodesToHTMLConverter { final List nodes; final int? startOffset; final int? endOffset; + final List _result = []; + html.Element? _stashListContainer; NodesToHTMLConverter({required this.nodes, this.startOffset, this.endOffset}); List toHTMLNodes() { - final result = []; for (final node in nodes) { if (node.type == "text") { final textNode = node as TextNode; if (node == nodes.first) { - result.add(_textNodeToHtml(textNode)); + _addTextNode(textNode); } else if (node == nodes.last) { - result.add(_textNodeToHtml(textNode, end: endOffset)); + _addTextNode(textNode, end: endOffset); } else { - result.add(_textNodeToHtml(textNode)); + _addTextNode(textNode); } } // TODO: handle image and other blocks } - return result; + return _result; + } + + _addTextNode(TextNode textNode, {int? end}) { + _addElement(textNode, _textNodeToHtml(textNode, end: end)); + } + + _addElement(TextNode textNode, html.Element element) { + if (element.localName == tagList) { + final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList; + _stashListContainer ??= html.Element.tag(isNumbered ? "ol" : "ul"); + _stashListContainer?.append(element); + } else { + if (_stashListContainer != null) { + _result.add(_stashListContainer!); + _stashListContainer = null; + } + _result.add(element); + } } String toHTMLString() { final elements = toHTMLNodes(); - final copyString = _HTMLNormalizer(elements).normalize().fold( + final copyString = elements.fold( "", ((previousValue, element) => previousValue + stringify(element))); return copyString; } From 430e9974be3595f322c6afb56c23ed341be8ff2d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 13:53:10 +0800 Subject: [PATCH 035/224] feat: convert number list --- .../lib/infra/html_converter.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index a297a12db1..9c45324651 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -170,7 +170,8 @@ class HTMLToNodesConverter { } } - final textNode = TextNode(type: "text", delta: delta); + final textNode = + TextNode(type: "text", delta: delta, attributes: attributes); if (isCheckbox) { textNode.attributes["subtype"] = StyleKey.checkbox; textNode.attributes["checkbox"] = checked; @@ -198,10 +199,11 @@ class HTMLToNodesConverter { List _handleOrderedList(html.Element element) { final result = []; - element.children.forEach((child) { - result - .addAll(_handleListElement(child, {"subtype": StyleKey.numberList})); - }); + for (var i = 0; i < element.children.length; i++) { + final child = element.children[i]; + result.addAll(_handleListElement( + child, {"subtype": StyleKey.numberList, "number": i + 1})); + } return result; } @@ -253,6 +255,10 @@ class NodesToHTMLConverter { } // TODO: handle image and other blocks } + if (_stashListContainer != null) { + _result.add(_stashListContainer!); + _stashListContainer = null; + } return _result; } @@ -263,7 +269,8 @@ class NodesToHTMLConverter { _addElement(TextNode textNode, html.Element element) { if (element.localName == tagList) { final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList; - _stashListContainer ??= html.Element.tag(isNumbered ? "ol" : "ul"); + _stashListContainer ??= + html.Element.tag(isNumbered ? tagOrderedList : tagUnorderedList); _stashListContainer?.append(element); } else { if (_stashListContainer != null) { From 20fb71455070717302c1461525a31374ceed0586 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 14:01:57 +0800 Subject: [PATCH 036/224] refactor: rename highlightColor to backgroundColor --- .../flowy_editor/example/assets/example.json | 2 +- .../lib/infra/html_converter.dart | 60 +++++++++++++++++-- .../lib/render/rich_text/rich_text_style.dart | 14 ++--- .../copy_paste_handler.dart | 12 +++- 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index fe74b22dad..e6357d93f9 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -73,7 +73,7 @@ }, { "insert": " / ", - "attributes": { "highlightColor": "0xFFFFFF00" } + "attributes": { "backgroundColor": "0xFFFFFF00" } }, { "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc." diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 9c45324651..7117c82eaa 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -4,6 +4,7 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; @@ -239,7 +240,28 @@ class NodesToHTMLConverter { final List _result = []; html.Element? _stashListContainer; - NodesToHTMLConverter({required this.nodes, this.startOffset, this.endOffset}); + NodesToHTMLConverter( + {required this.nodes, this.startOffset, this.endOffset}) { + if (nodes.isEmpty) { + return; + } else if (nodes.length == 1) { + final first = nodes.first; + if (first is TextNode) { + nodes[0] = first.copyWith( + delta: first.delta.slice(startOffset ?? 0, endOffset)); + } + } else { + final first = nodes.first; + final last = nodes.last; + if (first is TextNode) { + nodes[0] = first.copyWith(delta: first.delta.slice(startOffset ?? 0)); + } + if (last is TextNode) { + nodes[nodes.length - 1] = + last.copyWith(delta: last.delta.slice(0, endOffset)); + } + } + } List toHTMLNodes() { for (final node in nodes) { @@ -296,6 +318,26 @@ class NodesToHTMLConverter { checked: textNode.attributes["checkbox"] == true); } + String _attributesToCssStyle(Map attributes) { + final cssMap = {}; + if (attributes[StyleKey.backgroundColor] != null) { + cssMap["background-color"] = attributes[StyleKey.backgroundColor]; + } else if (attributes[StyleKey.color] != null) { + cssMap["color"] = attributes[StyleKey.color]; + } + return _cssMapToCssStyle(cssMap); + } + + String _cssMapToCssStyle(Map cssMap) { + return cssMap.entries.fold("", (previousValue, element) { + final kv = '${element.key}: ${element.value}'; + if (previousValue.isEmpty) { + return kv; + } + return '$previousValue; $kv"'; + }); + } + html.Element _deltaToHtml(Delta delta, {String? subType, int? end, bool? checked}) { if (end != null) { @@ -319,9 +361,19 @@ class NodesToHTMLConverter { if (op is TextInsert) { final attributes = op.attributes; if (attributes != null && attributes[StyleKey.bold] == true) { - final strong = html.Element.tag("strong"); - strong.append(html.Text(op.content)); - childNodes.add(strong); + if (attributes.length == 1 && attributes[StyleKey.bold] == true) { + final strong = html.Element.tag(tagStrong); + strong.append(html.Text(op.content)); + childNodes.add(strong); + } else { + final span = html.Element.tag(tagSpan); + final cssString = _attributesToCssStyle(attributes); + if (cssString.isNotEmpty) { + span.attributes["style"] = cssString; + } + span.append(html.Text(op.content)); + childNodes.add(span); + } } else { childNodes.add(html.Text(op.content)); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index c44fd8dac1..c9f0905b38 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -22,7 +22,7 @@ class StyleKey { static String underline = 'underline'; static String strikethrough = 'strikethrough'; static String color = 'color'; - static String highlightColor = 'highlightColor'; + static String backgroundColor = 'backgroundColor'; static String font = 'font'; static String href = 'href'; @@ -151,11 +151,11 @@ extension DeltaAttributesExtensions on Attributes { return null; } - Color? get highlightColor { - if (containsKey(StyleKey.highlightColor) && - this[StyleKey.highlightColor] is String) { + Color? get backgroundColor { + if (containsKey(StyleKey.backgroundColor) && + this[StyleKey.backgroundColor] is String) { return Color( - int.parse(this[StyleKey.highlightColor]), + int.parse(this[StyleKey.backgroundColor]), ); } return null; @@ -266,8 +266,8 @@ class RichTextStyle { } Color? get _backgroundColor { - if (attributes.highlightColor != null) { - return attributes.highlightColor!; + if (attributes.backgroundColor != null) { + return attributes.backgroundColor!; } else if (attributes.code) { return Colors.grey.withOpacity(0.4); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart index 4174e8eaee..34681ea21f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart @@ -15,7 +15,11 @@ _handleCopy(EditorState editorState) async { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { final textNode = nodeAtPath as TextNode; - final htmlString = NodesToHTMLConverter(nodes: [textNode]).toHTMLString(); + final htmlString = NodesToHTMLConverter( + nodes: [textNode], + startOffset: selection.start.offset, + endOffset: selection.end.offset) + .toHTMLString(); debugPrint('copy html: $htmlString'); RichClipboard.setData(RichClipboardData(html: htmlString)); } else { @@ -29,7 +33,11 @@ _handleCopy(EditorState editorState) async { final nodes = NodeIterator(editorState.document, beginNode, endNode).toList(); - final copyString = NodesToHTMLConverter(nodes: nodes).toHTMLString(); + final copyString = NodesToHTMLConverter( + nodes: nodes, + startOffset: selection.start.offset, + endOffset: selection.end.offset) + .toHTMLString(); debugPrint('copy html: $copyString'); RichClipboard.setData(RichClipboardData(html: copyString)); } From 79591791c1bdb17eb36b3813b9fa6b7712bf7e23 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 10 Aug 2022 14:05:31 +0800 Subject: [PATCH 037/224] chore: move all code into src/ --- .../flowy_editor/example/lib/main.dart | 6 ++- .../flowy_editor/lib/flowy_editor.dart | 26 +++++------ .../lib/{ => src}/document/attributes.dart | 0 .../lib/{ => src}/document/node.dart | 6 +-- .../lib/{ => src}/document/node_iterator.dart | 2 +- .../lib/{ => src}/document/path.dart | 0 .../lib/{ => src}/document/position.dart | 0 .../lib/{ => src}/document/selection.dart | 6 +-- .../lib/{ => src}/document/state_tree.dart | 6 +-- .../lib/{ => src}/document/text_delta.dart | 2 +- .../lib/{ => src}/editor_state.dart | 14 +++--- .../{ => src}/extensions/node_extensions.dart | 10 ++-- .../extensions/object_extensions.dart | 0 .../{ => src}/extensions/path_extensions.dart | 2 +- .../extensions/text_node_extensions.dart | 12 ++--- .../lib/{ => src}/infra/flowy_svg.dart | 0 .../lib/{ => src}/infra/html_converter.dart | 6 +-- .../lib/{ => src}/operation/operation.dart | 2 +- .../lib/{ => src}/operation/transaction.dart | 2 +- .../operation/transaction_builder.dart | 18 ++++---- .../{ => src}/render/editor/editor_entry.dart | 6 +-- .../render/rich_text/bulleted_list_text.dart | 16 +++---- .../render/rich_text/checkbox_text.dart | 18 ++++---- .../render/rich_text/default_selectable.dart | 6 +-- .../render/rich_text/flowy_rich_text.dart | 18 ++++---- .../render/rich_text/heading_text.dart | 14 +++--- .../render/rich_text/number_list_text.dart | 16 +++---- .../render/rich_text/quoted_text.dart | 16 +++---- .../{ => src}/render/rich_text/rich_text.dart | 16 +++---- .../render/rich_text/rich_text_style.dart | 4 +- .../render/selection/cursor_widget.dart | 0 .../render/selection/selectable.dart | 4 +- .../render/selection/selection_widget.dart | 0 .../render/selection/toolbar_widget.dart | 8 ++-- .../format_rich_text_style.dart | 16 +++---- .../lib/{ => src}/service/editor_service.dart | 46 +++++++++---------- .../lib/{ => src}/service/input_service.dart | 10 ++-- .../arrow_keys_handler.dart | 2 +- .../copy_paste_handler.dart | 6 +-- .../delete_nodes_handler.dart | 2 +- .../delete_text_handler.dart | 2 +- ...er_without_shift_in_text_node_handler.dart | 16 +++---- .../redo_undo_handler.dart | 2 +- .../slash_handler.dart | 16 +++---- ...pdate_text_style_by_command_x_handler.dart | 6 +-- .../whitespace_handler.dart | 14 +++--- .../{ => src}/service/keyboard_service.dart | 2 +- .../service/render_plugin_service.dart | 4 +- .../lib/{ => src}/service/scroll_service.dart | 0 .../{ => src}/service/selection_service.dart | 20 ++++---- .../lib/{ => src}/service/service.dart | 10 ++-- .../{ => src}/service/toolbar_service.dart | 2 +- .../lib/{ => src}/undo_manager.dart | 10 ++-- .../flowy_editor/test/delta_test.dart | 2 +- .../flowy_editor/test/flowy_editor_test.dart | 10 ++-- .../flowy_editor/test/operation_test.dart | 10 ++-- 56 files changed, 236 insertions(+), 234 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/attributes.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/node.dart (96%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/node_iterator.dart (96%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/path.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/position.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/selection.dart (89%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/state_tree.dart (91%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/document/text_delta.dart (99%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/editor_state.dart (87%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/extensions/node_extensions.dart (70%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/extensions/object_extensions.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/extensions/path_extensions.dart (92%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/extensions/text_node_extensions.dart (88%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/infra/flowy_svg.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/infra/html_converter.dart (97%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/operation/operation.dart (98%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/operation/transaction.dart (95%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/operation/transaction_builder.dart (90%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/editor/editor_entry.dart (88%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/bulleted_list_text.dart (80%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/checkbox_text.dart (88%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/default_selectable.dart (85%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/flowy_rich_text.dart (91%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/heading_text.dart (85%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/number_list_text.dart (80%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/quoted_text.dart (81%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/rich_text.dart (73%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/rich_text/rich_text_style.dart (98%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/selection/cursor_widget.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/selection/selectable.dart (91%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/selection/selection_widget.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/render/selection/toolbar_widget.dart (95%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/default_text_operations/format_rich_text_style.dart (88%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/editor_service.dart (62%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/input_service.dart (96%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/arrow_keys_handler.dart (98%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/copy_paste_handler.dart (98%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/delete_nodes_handler.dart (89%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/delete_text_handler.dart (97%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart (88%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/redo_undo_handler.dart (86%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/slash_handler.dart (94%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart (83%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/internal_key_event_handlers/whitespace_handler.dart (88%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/keyboard_service.dart (97%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/render_plugin_service.dart (97%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/scroll_service.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/selection_service.dart (96%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/service.dart (83%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/service/toolbar_service.dart (94%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => src}/undo_manager.dart (90%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 3dc9fe4806..5db7288c76 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,12 +1,14 @@ import 'dart:collection'; import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:example/expandable_floating_action_button.dart'; import 'package:example/plugin/image_node_widget.dart'; import 'package:example/plugin/youtube_link_node_widget.dart'; -import 'package:flutter/material.dart'; + import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/services.dart'; void main() { runApp(const MyApp()); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index c3e15959a6..a767c08407 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -1,15 +1,15 @@ library flowy_editor; -export 'package:flowy_editor/document/state_tree.dart'; -export 'package:flowy_editor/document/node.dart'; -export 'package:flowy_editor/document/path.dart'; -export 'package:flowy_editor/document/text_delta.dart'; -export 'package:flowy_editor/render/selection/selectable.dart'; -export 'package:flowy_editor/operation/transaction.dart'; -export 'package:flowy_editor/operation/transaction_builder.dart'; -export 'package:flowy_editor/operation/operation.dart'; -export 'package:flowy_editor/editor_state.dart'; -export 'package:flowy_editor/service/editor_service.dart'; -export 'package:flowy_editor/document/selection.dart'; -export 'package:flowy_editor/document/position.dart'; -export 'package:flowy_editor/service/render_plugin_service.dart'; +export 'src/document/state_tree.dart'; +export 'src/document/node.dart'; +export 'src/document/path.dart'; +export 'src/document/text_delta.dart'; +export 'src/render/selection/selectable.dart'; +export 'src/operation/transaction.dart'; +export 'src/operation/transaction_builder.dart'; +export 'src/operation/operation.dart'; +export 'src/editor_state.dart'; +export 'src/service/editor_service.dart'; +export 'src/document/selection.dart'; +export 'src/document/position.dart'; +export 'src/service/render_plugin_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart similarity index 96% rename from frontend/app_flowy/packages/flowy_editor/lib/document/node.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 21883362b1..0b6b941aaa 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -1,7 +1,7 @@ import 'dart:collection'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/text_delta.dart'; -import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; +import 'package:flowy_editor/src/operation/operation.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node_iterator.dart similarity index 96% rename from frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/node_iterator.dart index bafe106f27..c1349b7c4c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node_iterator.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node_iterator.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/src/document/node.dart'; import './state_tree.dart'; import './node.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/path.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/document/path.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/path.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/position.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/document/position.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/position.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart similarity index 89% rename from frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart index 16341cee1a..692cc8c91b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart @@ -1,6 +1,6 @@ -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/extensions/path_extensions.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/extensions/path_extensions.dart'; class Selection { final Position start; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart similarity index 91% rename from frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart index cf49f48ac8..199736234a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart @@ -1,6 +1,6 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; import './attributes.dart'; class StateTree { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart similarity index 99% rename from frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 64335d4a05..72378f80c6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'dart:math'; -import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart similarity index 87% rename from frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 5ea49c644d..82725b5255 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -1,13 +1,13 @@ import 'dart:async'; -import 'package:flowy_editor/service/service.dart'; +import 'package:flowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/state_tree.dart'; -import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/operation/transaction.dart'; -import 'package:flowy_editor/undo_manager.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/document/state_tree.dart'; +import 'package:flowy_editor/src/operation/operation.dart'; +import 'package:flowy_editor/src/operation/transaction.dart'; +import 'package:flowy_editor/src/undo_manager.dart'; class ApplyOptions { /// This flag indicates that diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/node_extensions.dart similarity index 70% rename from frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/extensions/node_extensions.dart index 0ca9ba08a6..db58f8a3db 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/node_extensions.dart @@ -1,8 +1,8 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/extensions/path_extensions.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/extensions/object_extensions.dart'; +import 'package:flowy_editor/src/extensions/path_extensions.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; extension NodeExtensions on Node { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/object_extensions.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/extensions/object_extensions.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/extensions/object_extensions.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/path_extensions.dart similarity index 92% rename from frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/extensions/path_extensions.dart index 793dc552dd..7f72444224 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/path_extensions.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/src/document/path.dart'; import 'dart:math'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart similarity index 88% rename from frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart index 29e90784ae..7b068f18d7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart @@ -1,9 +1,9 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/text_delta.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; extension TextNodeExtension on TextNode { bool allSatisfyBoldInSelection(Selection selection) => diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/flowy_svg.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/infra/flowy_svg.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/infra/flowy_svg.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart similarity index 97% rename from frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 708e47cb85..d05f768895 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -1,8 +1,8 @@ import 'dart:collection'; -import 'package:flowy_editor/document/attributes.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/operation.dart similarity index 98% rename from frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/operation/operation.dart index 58fe8e2f14..fc5e7c66a3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/operation.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/document/attributes.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; import 'package:flowy_editor/flowy_editor.dart'; abstract class Operation { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction.dart similarity index 95% rename from frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction.dart index 5dcf167628..76e375e5f8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction.dart @@ -1,6 +1,6 @@ import 'dart:collection'; import 'package:flutter/material.dart'; -import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/src/document/selection.dart'; import './operation.dart'; /// A [Transaction] has a list of [Operation] objects that will be applied diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart similarity index 90% rename from frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index cececec924..4c080f06e2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -1,15 +1,15 @@ import 'dart:collection'; import 'dart:math'; -import 'package:flowy_editor/document/attributes.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/text_delta.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/operation/operation.dart'; +import 'package:flowy_editor/src/operation/transaction.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save make a snapshot of the cursor selection state automatically. diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/editor/editor_entry.dart similarity index 88% rename from frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/editor/editor_entry.dart index 9be82fa31a..90ee326e8b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/editor/editor_entry.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; class EditorEntryWidgetBuilder extends NodeWidgetBuilder { @override diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/bulleted_list_text.dart similarity index 80% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 2607be26ed..9586f1192c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -1,11 +1,11 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/rich_text/default_selectable.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart similarity index 88% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart index 073c339ed6..c7ba8607a6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -1,12 +1,12 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/rich_text/default_selectable.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/default_selectable.dart similarity index 85% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/default_selectable.dart index 7ea93509c1..0a0ed09f05 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/default_selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/default_selectable.dart @@ -1,6 +1,6 @@ -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; mixin DefaultSelectable { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart similarity index 91% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index a041d179b9..350a5c71e6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/text_delta.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/heading_text.dart similarity index 85% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/heading_text.dart index c010ad4833..ddc7537476 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/heading_text.dart @@ -1,10 +1,10 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/render/rich_text/default_selectable.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/number_list_text.dart similarity index 80% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/number_list_text.dart index 4ffd587470..bd08a30d80 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -1,11 +1,11 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/rich_text/default_selectable.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/quoted_text.dart similarity index 81% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/quoted_text.dart index 09004f7f9d..59cafd82af 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -1,11 +1,11 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/rich_text/default_selectable.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text.dart similarity index 73% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text.dart index bfb4c217a7..765c3b95d4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text.dart @@ -1,11 +1,11 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/render/rich_text/default_selectable.dart'; -import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/flowy_rich_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flutter/material.dart'; class RichTextNodeWidgetBuilder extends NodeWidgetBuilder { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart similarity index 98% rename from frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart index c44fd8dac1..c924709948 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -1,5 +1,5 @@ -import 'package:flowy_editor/document/attributes.dart'; -import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; +import 'package:flowy_editor/src/document/node.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/cursor_widget.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/cursor_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/cursor_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart similarity index 91% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart index 58377dcc02..bdea01b895 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart @@ -1,5 +1,5 @@ -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; import 'package:flutter/material.dart'; /// diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selection_widget.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/selection_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selection_widget.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart similarity index 95% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart index 4423e674dc..68d78f484f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart @@ -1,9 +1,9 @@ -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; typedef ToolbarEventHandler = void Function(EditorState editorState); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart similarity index 88% rename from frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index ce6d733d73..3dcc519274 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -1,11 +1,11 @@ -import 'package:flowy_editor/document/attributes.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/extensions/text_node_extensions.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; void formatText(EditorState editorState) { formatTextNodes(editorState, {}); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart similarity index 62% rename from frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart index 2e38d5bbfb..b596f83c2a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart @@ -1,28 +1,28 @@ import 'package:flutter/material.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/render/editor/editor_entry.dart'; -import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart'; -import 'package:flowy_editor/render/rich_text/checkbox_text.dart'; -import 'package:flowy_editor/render/rich_text/heading_text.dart'; -import 'package:flowy_editor/render/rich_text/number_list_text.dart'; -import 'package:flowy_editor/render/rich_text/quoted_text.dart'; -import 'package:flowy_editor/render/rich_text/rich_text.dart'; -import 'package:flowy_editor/service/input_service.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/whitespace_handler.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/redo_undo_handler.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/scroll_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flowy_editor/service/toolbar_service.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/render/editor/editor_entry.dart'; +import 'package:flowy_editor/src/render/rich_text/bulleted_list_text.dart'; +import 'package:flowy_editor/src/render/rich_text/checkbox_text.dart'; +import 'package:flowy_editor/src/render/rich_text/heading_text.dart'; +import 'package:flowy_editor/src/render/rich_text/number_list_text.dart'; +import 'package:flowy_editor/src/render/rich_text/quoted_text.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text.dart'; +import 'package:flowy_editor/src/service/input_service.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/service/scroll_service.dart'; +import 'package:flowy_editor/src/service/selection_service.dart'; +import 'package:flowy_editor/src/service/toolbar_service.dart'; NodeWidgetBuilders defaultBuilders = { 'editor': EditorEntryWidgetBuilder(), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart similarity index 96% rename from frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart index 824fc07230..c52a411905 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/extensions/node_extensions.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; mixin FlowyInputService { void attach(TextEditingValue textEditingValue); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart similarity index 98% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index 7fbdf669b5..83243d2dc7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,5 +1,5 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart similarity index 98% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index d3eeb073b9..57cc47d605 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,8 +1,8 @@ import 'package:html/dom.dart' as html; import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/infra/html_converter.dart'; -import 'package:flowy_editor/document/node_iterator.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; +import 'package:flowy_editor/src/infra/html_converter.dart'; +import 'package:flowy_editor/src/document/node_iterator.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart similarity index 89% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart index dda52612e9..132f88854a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart @@ -1,5 +1,5 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flutter/material.dart'; FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart similarity index 97% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index e44f0002a0..1bb6872ff4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; // Handle delete text. FlowyKeyEventHandler deleteTextHandler = (editorState, event) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart similarity index 88% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 51e593a20b..136c06ab14 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_editor/document/attributes.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/extensions/path_extensions.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/document/attributes.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/extensions/path_extensions.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; /// Handle some cases where enter is pressed and shift is not pressed. /// diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart similarity index 86% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart index 75b22402e4..37892d9572 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/redo_undo_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/redo_undo_handler.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart similarity index 94% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 4d77ef1ebc..6a265808fe 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -1,13 +1,13 @@ import 'dart:math'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/infra/flowy_svg.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; +import 'package:flowy_editor/src/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart similarity index 83% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 02073563eb..4bd9382e3a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { if (!event.isMetaPressed || event.character == null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart similarity index 88% rename from frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index b3642cc1a1..41574e6aaa 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; const _bulletedListSymbols = ['*', '-']; const _checkboxListSymbols = ['[x]', '-[x]']; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart similarity index 97% rename from frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart index 01cc0214a1..81ce8348a4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart @@ -1,6 +1,6 @@ +import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/services.dart'; -import '../editor_state.dart'; import 'package:flutter/material.dart'; mixin FlowyKeyboardService on State { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart similarity index 97% rename from frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart index 8ac32ac66c..e973d16d6a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart @@ -1,5 +1,5 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/editor_state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/scroll_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart similarity index 96% rename from frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index 73a66dbc7a..19d355847f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -4,16 +4,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/node_iterator.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/state_tree.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; -import 'package:flowy_editor/render/selection/cursor_widget.dart'; -import 'package:flowy_editor/render/selection/selectable.dart'; -import 'package:flowy_editor/render/selection/selection_widget.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/node_iterator.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/document/state_tree.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/extensions/node_extensions.dart'; +import 'package:flowy_editor/src/render/selection/cursor_widget.dart'; +import 'package:flowy_editor/src/render/selection/selectable.dart'; +import 'package:flowy_editor/src/render/selection/selection_widget.dart'; /// Process selection and cursor mixin FlowySelectionService on State { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart similarity index 83% rename from frontend/app_flowy/packages/flowy_editor/lib/service/service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart index e36312d7f3..cdf137bee8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/service/render_plugin_service.dart'; -import 'package:flowy_editor/service/scroll_service.dart'; -import 'package:flowy_editor/service/selection_service.dart'; -import 'package:flowy_editor/service/toolbar_service.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; +import 'package:flowy_editor/src/service/render_plugin_service.dart'; +import 'package:flowy_editor/src/service/scroll_service.dart'; +import 'package:flowy_editor/src/service/selection_service.dart'; +import 'package:flowy_editor/src/service/toolbar_service.dart'; class FlowyService { // selection service diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart similarity index 94% rename from frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart index f2026acb23..a45fe8b778 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/render/selection/toolbar_widget.dart'; +import 'package:flowy_editor/src/render/selection/toolbar_widget.dart'; mixin FlowyToolbarService { /// Show the toolbar widget beside the offset. diff --git a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart similarity index 90% rename from frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart rename to frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart index 5b543f03a1..b81e91481f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart @@ -1,10 +1,10 @@ import 'dart:collection'; -import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/operation/transaction.dart'; -import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/src/document/selection.dart'; +import 'package:flowy_editor/src/operation/operation.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/operation/transaction.dart'; +import 'package:flowy_editor/src/editor_state.dart'; import 'package:flutter/foundation.dart'; /// A [HistoryItem] contains list of operations committed by users. diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 9a914888d4..89001e79cf 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/src/document/text_delta.dart'; void main() { group('compose', () { diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 49d0fd00f5..e1b97a591d 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -1,10 +1,10 @@ import 'dart:convert'; -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/document/state_tree.dart'; -import 'package:flowy_editor/document/path.dart'; -import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/src/document/node.dart'; +import 'package:flowy_editor/src/document/state_tree.dart'; +import 'package:flowy_editor/src/document/path.dart'; +import 'package:flowy_editor/src/document/position.dart'; +import 'package:flowy_editor/src/document/selection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index 339807cea4..6e97ce9930 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -1,11 +1,11 @@ import 'dart:collection'; -import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/src/document/node.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flowy_editor/operation/operation.dart'; -import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flowy_editor/src/operation/operation.dart'; +import 'package:flowy_editor/src/operation/transaction_builder.dart'; +import 'package:flowy_editor/src/editor_state.dart'; +import 'package:flowy_editor/src/document/state_tree.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); From 046faf38802448aa7841089d6084f92e0e0ac5f8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 10 Aug 2022 15:07:30 +0800 Subject: [PATCH 038/224] fix: unexpect behaviour when pressing enter key --- .../enter_without_shift_in_text_node_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 136c06ab14..39c74d2eab 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -67,7 +67,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = // If selection is collapsed and position.start.offset == 0, // insert a empty text node before. if (selection.isCollapsed && selection.start.offset == 0) { - if (textNode.toRawString().isEmpty) { + if (textNode.toRawString().isEmpty && textNode.subtype != null) { final afterSelection = Selection.collapsed( Position(path: textNode.path, offset: 0), ); From c2a295d9cda911a428c235d8b68d5989d0a23f40 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 14:36:39 +0800 Subject: [PATCH 039/224] feat: copy styles of text delta --- .../lib/infra/html_converter.dart | 103 ++++++++++++++---- 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart index 7117c82eaa..db016c5fc5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/infra/html_converter.dart @@ -4,7 +4,7 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; +import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; @@ -22,6 +22,12 @@ const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; +extension on Color { + String toRgbaString() { + return 'rgba($red, $green, $blue, $alpha)'; + } +} + /// Converting the HTML to nodes class HTMLToNodesConverter { final html.Document _document; @@ -104,29 +110,78 @@ class HTMLToNodesConverter { return node; } + Map _cssStringToMap(String? cssString) { + final result = {}; + if (cssString == null) { + return result; + } + + final entries = cssString.split(";"); + for (final entry in entries) { + final tuples = entry.split(":"); + if (tuples.length < 2) { + continue; + } + result[tuples[0]] = tuples[1]; + } + + return result; + } + Attributes? _getDeltaAttributesFromHtmlAttributes( LinkedHashMap htmlAttributes) { final attrs = {}; final styleString = htmlAttributes["style"]; - if (styleString != null) { - final entries = styleString.split(";"); - for (final entry in entries) { - final tuples = entry.split(":"); - if (tuples.length < 2) { - continue; - } - if (tuples[0] == "font-weight") { - int? weight = int.tryParse(tuples[1]); - if (weight != null && weight > 500) { - attrs["bold"] = true; - } - } + final cssMap = _cssStringToMap(styleString); + + final fontWeightStr = cssMap["font-weight"]; + if (fontWeightStr != null) { + int? weight = int.tryParse(fontWeightStr); + if (weight != null && weight > 500) { + attrs["bold"] = true; } } + final backgroundColorStr = cssMap["background-color"]; + final backgroundColor = _tryParseCssColorString(backgroundColorStr); + if (backgroundColor != null) { + attrs[StyleKey.backgroundColor] = + '0x${backgroundColor.value.toRadixString(16)}'; + } + return attrs.isEmpty ? null : attrs; } + Color? _tryParseCssColorString(String? colorString) { + if (colorString == null) { + return null; + } + final reg = RegExp(r'rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)'); + final match = reg.firstMatch(colorString); + if (match == null) { + return null; + } + + if (match.groupCount < 4) { + return null; + } + final redStr = match.group(1); + final greenStr = match.group(2); + final blueStr = match.group(3); + final alphaStr = match.group(4); + + final red = redStr != null ? int.tryParse(redStr) : null; + final green = greenStr != null ? int.tryParse(greenStr) : null; + final blue = blueStr != null ? int.tryParse(blueStr) : null; + final alpha = alphaStr != null ? int.tryParse(alphaStr) : null; + + if (red == null || green == null || blue == null || alpha == null) { + return null; + } + + return Color.fromARGB(alpha, red, green, blue); + } + _handleRichTextElement(Delta delta, html.Element element) { if (element.localName == tagSpan) { delta.insert(element.text, @@ -321,9 +376,19 @@ class NodesToHTMLConverter { String _attributesToCssStyle(Map attributes) { final cssMap = {}; if (attributes[StyleKey.backgroundColor] != null) { - cssMap["background-color"] = attributes[StyleKey.backgroundColor]; - } else if (attributes[StyleKey.color] != null) { - cssMap["color"] = attributes[StyleKey.color]; + final color = Color( + int.parse(attributes[StyleKey.backgroundColor]), + ); + cssMap["background-color"] = color.toRgbaString(); + } + if (attributes[StyleKey.color] != null) { + final color = Color( + int.parse(attributes[StyleKey.color]), + ); + cssMap["color"] = color.toRgbaString(); + } + if (attributes[StyleKey.bold] == true) { + cssMap["font-weight"] = "bold"; } return _cssMapToCssStyle(cssMap); } @@ -334,7 +399,7 @@ class NodesToHTMLConverter { if (previousValue.isEmpty) { return kv; } - return '$previousValue; $kv"'; + return '$previousValue; $kv'; }); } @@ -360,7 +425,7 @@ class NodesToHTMLConverter { for (final op in delta.operations) { if (op is TextInsert) { final attributes = op.attributes; - if (attributes != null && attributes[StyleKey.bold] == true) { + if (attributes != null) { if (attributes.length == 1 && attributes[StyleKey.bold] == true) { final strong = html.Element.tag(tagStrong); strong.append(html.Text(op.content)); From 9930706d9a354074f882ac902295e8318ab85a5a Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 9 Aug 2022 19:06:15 +0800 Subject: [PATCH 040/224] refactor: separate cache from service file --- .../grid/application/block/block_cache.dart | 6 +- .../cell_service/cell_field_notifier.dart | 13 +- .../cell/cell_service/cell_service.dart | 3 +- .../cell/cell_service/context_builder.dart | 13 +- .../plugins/grid/application/grid_bloc.dart | 2 +- .../application/grid_data_controller.dart | 2 + .../grid/application/grid_header_bloc.dart | 3 +- .../grid/application/grid_service.dart | 190 ---------- .../row/row_action_sheet_bloc.dart | 2 + .../grid/application/row/row_bloc.dart | 1 + .../application/row/row_data_controller.dart | 4 +- .../grid/application/row/row_detail_bloc.dart | 3 +- .../grid/application/row/row_service.dart | 326 ------------------ .../application/setting/property_bloc.dart | 3 +- .../plugins/grid/presentation/grid_page.dart | 2 +- .../widgets/cell/cell_builder.dart | 2 +- .../widgets/cell/checkbox_cell.dart | 4 +- .../widgets/cell/date_cell/date_cell.dart | 4 +- .../widgets/cell/number_cell.dart | 4 +- .../select_option_cell.dart | 8 +- .../presentation/widgets/cell/text_cell.dart | 4 +- .../widgets/cell/url_cell/url_cell.dart | 4 +- .../widgets/header/grid_header.dart | 1 + .../presentation/widgets/row/grid_row.dart | 1 + .../widgets/row/row_action_sheet.dart | 2 +- .../presentation/widgets/row/row_detail.dart | 2 +- .../widgets/toolbar/grid_property.dart | 2 +- .../widgets/toolbar/grid_setting.dart | 2 +- .../widgets/toolbar/grid_toolbar.dart | 2 +- .../app_flowy/lib/startup/deps_resolver.dart | 2 + 30 files changed, 57 insertions(+), 560 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart index 5ccff7c838..5569311228 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; -import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import '../field/field_cache.dart'; +import '../row/row_cache.dart'; import 'block_listener.dart'; /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information @@ -24,7 +24,7 @@ class GridBlockCache { _rowCache = GridRowCache( gridId: gridId, block: block, - notifier: GridRowCacheFieldNotifierImpl(fieldCache), + notifier: GridRowFieldNotifierImpl(fieldCache), ); _listener = GridBlockListener(blockId: block.id); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart index 950832c674..1b2393671c 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart @@ -3,20 +3,22 @@ import 'package:flutter/foundation.dart'; import 'cell_service.dart'; -abstract class IGridFieldChangedNotifier { - void onFieldChanged(void Function(GridFieldPB) callback); - void dispose(); +abstract class IGridCellFieldNotifier { + void onCellFieldChanged(void Function(GridFieldPB) callback); + void onCellDispose(); } /// GridPB's cell helper wrapper that enables each cell will get notified when the corresponding field was changed. /// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen. class GridCellFieldNotifier { + final IGridCellFieldNotifier notifier; + /// fieldId: {objectId: callback} final Map>> _fieldListenerByFieldId = {}; - GridCellFieldNotifier({required IGridFieldChangedNotifier notifier}) { - notifier.onFieldChanged( + GridCellFieldNotifier({required this.notifier}) { + notifier.onCellFieldChanged( (field) { final map = _fieldListenerByFieldId[field.id]; if (map != null) { @@ -56,6 +58,7 @@ class GridCellFieldNotifier { } Future dispose() async { + notifier.onCellDispose(); _fieldListenerByFieldId.clear(); } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart index 2bef94c16a..106db1e754 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart @@ -1,7 +1,5 @@ import 'dart:async'; import 'dart:collection'; - -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; @@ -18,6 +16,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_listener.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'dart:convert' show utf8; +import '../../field/field_cache.dart'; import '../../field/type_option/type_option_service.dart'; import 'cell_field_notifier.dart'; part 'cell_service.freezed.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 526213ee4d..12cadcca40 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -21,8 +21,8 @@ class GridCellControllerBuilder { _cellId = cellId; IGridCellController build() { - final cellFieldNotifier = GridCellFieldNotifier( - notifier: _GridFieldChangedNotifierImpl(_fieldCache)); + final cellFieldNotifier = + GridCellFieldNotifier(notifier: GridCellFieldNotifierImpl(_fieldCache)); switch (_cellId.fieldType) { case FieldType.Checkbox: @@ -295,6 +295,7 @@ class IGridCellController extends Equatable { if (_onFieldChangedFn != null) { _fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!); + _fieldNotifier.dispose(); _onFieldChangedFn = null; } } @@ -304,14 +305,14 @@ class IGridCellController extends Equatable { [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id]; } -class _GridFieldChangedNotifierImpl extends IGridFieldChangedNotifier { +class GridCellFieldNotifierImpl extends IGridCellFieldNotifier { final GridFieldCache _cache; FieldChangesetCallback? _onChangesetFn; - _GridFieldChangedNotifierImpl(GridFieldCache cache) : _cache = cache; + GridCellFieldNotifierImpl(GridFieldCache cache) : _cache = cache; @override - void dispose() { + void onCellDispose() { if (_onChangesetFn != null) { _cache.removeListener(onChangesetListener: _onChangesetFn!); _onChangesetFn = null; @@ -319,7 +320,7 @@ class _GridFieldChangedNotifierImpl extends IGridFieldChangedNotifier { } @override - void onFieldChanged(void Function(GridFieldPB p1) callback) { + void onCellFieldChanged(void Function(GridFieldPB p1) callback) { _onChangesetFn = (GridFieldChangesetPB changeset) { for (final updatedField in changeset.updatedFields) { callback(updatedField); diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart index 4516d01ba3..44881b9f54 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart @@ -8,7 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'block/block_cache.dart'; import 'grid_data_controller.dart'; -import 'row/row_service.dart'; +import 'row/row_cache.dart'; import 'dart:collection'; part 'grid_bloc.freezed.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 3563a13528..4833bc3ae0 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -9,7 +9,9 @@ import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'dart:async'; import 'package:dartz/dartz.dart'; import 'block/block_cache.dart'; +import 'field/field_cache.dart'; import 'prelude.dart'; +import 'row/row_cache.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart index 06a6b791d8..a0ab1aa343 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart @@ -4,7 +4,8 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; -import 'grid_service.dart'; + +import 'field/field_cache.dart'; part 'grid_header_bloc.freezed.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index dff03f636e..6eaaab7be8 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -1,17 +1,11 @@ -import 'dart:collection'; - -import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; -import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; -import 'package:flutter/foundation.dart'; -import 'row/row_service.dart'; class GridService { final String gridId; @@ -46,187 +40,3 @@ class GridService { return FolderEventCloseView(request).send(); } } - -class FieldsNotifier extends ChangeNotifier { - List _fields = []; - - set fields(List fields) { - _fields = fields; - notifyListeners(); - } - - List get fields => _fields; -} - -typedef FieldChangesetCallback = void Function(GridFieldChangesetPB); -typedef FieldsCallback = void Function(List); - -class GridFieldCache { - final String gridId; - final GridFieldsListener _fieldListener; - FieldsNotifier? _fieldNotifier = FieldsNotifier(); - final Map _fieldsCallbackMap = {}; - final Map - _changesetCallbackMap = {}; - - GridFieldCache({required this.gridId}) - : _fieldListener = GridFieldsListener(gridId: gridId) { - _fieldListener.start(onFieldsChanged: (result) { - result.fold( - (changeset) { - _deleteFields(changeset.deletedFields); - _insertFields(changeset.insertedFields); - _updateFields(changeset.updatedFields); - for (final listener in _changesetCallbackMap.values) { - listener(changeset); - } - }, - (err) => Log.error(err), - ); - }); - } - - Future dispose() async { - await _fieldListener.stop(); - _fieldNotifier?.dispose(); - _fieldNotifier = null; - } - - UnmodifiableListView get unmodifiableFields => - UnmodifiableListView(_fieldNotifier?.fields ?? []); - - List get fields => [..._fieldNotifier?.fields ?? []]; - - set fields(List fields) { - _fieldNotifier?.fields = [...fields]; - } - - void addListener({ - FieldsCallback? onFields, - FieldChangesetCallback? onChangeset, - bool Function()? listenWhen, - }) { - if (onChangeset != null) { - fn(c) { - if (listenWhen != null && listenWhen() == false) { - return; - } - onChangeset(c); - } - - _changesetCallbackMap[onChangeset] = fn; - } - - if (onFields != null) { - fn() { - if (listenWhen != null && listenWhen() == false) { - return; - } - onFields(fields); - } - - _fieldsCallbackMap[onFields] = fn; - _fieldNotifier?.addListener(fn); - } - } - - void removeListener({ - FieldsCallback? onFieldsListener, - FieldChangesetCallback? onChangesetListener, - }) { - if (onFieldsListener != null) { - final fn = _fieldsCallbackMap.remove(onFieldsListener); - if (fn != null) { - _fieldNotifier?.removeListener(fn); - } - } - - if (onChangesetListener != null) { - _changesetCallbackMap.remove(onChangesetListener); - } - } - - void _deleteFields(List deletedFields) { - if (deletedFields.isEmpty) { - return; - } - final List newFields = fields; - final Map deletedFieldMap = { - for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder - }; - - newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); - _fieldNotifier?.fields = newFields; - } - - void _insertFields(List insertedFields) { - if (insertedFields.isEmpty) { - return; - } - final List newFields = fields; - for (final indexField in insertedFields) { - if (newFields.length > indexField.index) { - newFields.insert(indexField.index, indexField.field_1); - } else { - newFields.add(indexField.field_1); - } - } - _fieldNotifier?.fields = newFields; - } - - void _updateFields(List updatedFields) { - if (updatedFields.isEmpty) { - return; - } - final List newFields = fields; - for (final updatedField in updatedFields) { - final index = - newFields.indexWhere((field) => field.id == updatedField.id); - if (index != -1) { - newFields.removeAt(index); - newFields.insert(index, updatedField); - } - } - _fieldNotifier?.fields = newFields; - } -} - -class GridRowCacheFieldNotifierImpl extends GridRowCacheFieldNotifier { - final GridFieldCache _cache; - FieldChangesetCallback? _onChangesetFn; - FieldsCallback? _onFieldFn; - GridRowCacheFieldNotifierImpl(GridFieldCache cache) : _cache = cache; - - @override - UnmodifiableListView get fields => _cache.unmodifiableFields; - - @override - void onFieldsChanged(VoidCallback callback) { - _onFieldFn = (_) => callback(); - _cache.addListener(onFields: _onFieldFn); - } - - @override - void onFieldChanged(void Function(GridFieldPB) callback) { - _onChangesetFn = (GridFieldChangesetPB changeset) { - for (final updatedField in changeset.updatedFields) { - callback(updatedField); - } - }; - - _cache.addListener(onChangeset: _onChangesetFn); - } - - @override - void dispose() { - if (_onFieldFn != null) { - _cache.removeListener(onFieldsListener: _onFieldFn!); - _onFieldFn = null; - } - - if (_onChangesetFn != null) { - _cache.removeListener(onChangesetListener: _onChangesetFn!); - _onChangesetFn = null; - } - } -} diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart index 7881e485cb..cedd426348 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart @@ -6,6 +6,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import 'package:dartz/dartz.dart'; +import 'row_cache.dart'; + part 'row_action_sheet_bloc.freezed.dart'; class RowActionSheetBloc diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index b6079b6764..b2579930ad 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -5,6 +5,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; +import 'row_cache.dart'; import 'row_data_controller.dart'; import 'row_service.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart index fb05ff5920..6975bd6a51 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../cell/cell_service/cell_service.dart'; -import '../grid_service.dart'; -import 'row_service.dart'; +import '../field/field_cache.dart'; +import 'row_cache.dart'; typedef OnRowChanged = void Function(GridCellMap, GridRowChangeReason); diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart index 900b56f9a5..17f6ef4990 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart @@ -2,8 +2,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; -import 'row_service.dart'; - +import 'row_cache.dart'; part 'row_detail_bloc.freezed.dart'; class RowDetailBloc extends Bloc { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index d72a05089a..2dce917da4 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -1,283 +1,9 @@ -import 'dart:collection'; -import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; -import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -part 'row_service.freezed.dart'; - -typedef RowUpdateCallback = void Function(); - -abstract class GridRowCacheFieldNotifier { - UnmodifiableListView get fields; - void onFieldsChanged(VoidCallback callback); - void onFieldChanged(void Function(GridFieldPB) callback); - void dispose(); -} - -/// Cache the rows in memory -/// Insert / delete / update row -/// -/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. - -class GridRowCache { - final String gridId; - final GridBlockPB block; - - /// _rows containers the current block's rows - /// Use List to reverse the order of the GridRow. - List _rowInfos = []; - - /// Use Map for faster access the raw row data. - final HashMap _rowByRowId; - - final GridCellCache _cellCache; - final GridRowCacheFieldNotifier _fieldNotifier; - final _GridRowChangesetNotifier _rowChangeReasonNotifier; - - UnmodifiableListView get rows => UnmodifiableListView(_rowInfos); - GridCellCache get cellCache => _cellCache; - - GridRowCache({ - required this.gridId, - required this.block, - required GridRowCacheFieldNotifier notifier, - }) : _cellCache = GridCellCache(gridId: gridId), - _rowByRowId = HashMap(), - _rowChangeReasonNotifier = _GridRowChangesetNotifier(), - _fieldNotifier = notifier { - // - notifier.onFieldsChanged(() => _rowChangeReasonNotifier - .receive(const GridRowChangeReason.fieldDidChange())); - notifier.onFieldChanged((field) => _cellCache.remove(field.id)); - _rowInfos = block.rows - .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble())) - .toList(); - } - - Future dispose() async { - _fieldNotifier.dispose(); - _rowChangeReasonNotifier.dispose(); - await _cellCache.dispose(); - } - - void applyChangesets(List changesets) { - for (final changeset in changesets) { - _deleteRows(changeset.deletedRows); - _insertRows(changeset.insertedRows); - _updateRows(changeset.updatedRows); - _hideRows(changeset.hideRows); - _showRows(changeset.visibleRows); - } - } - - void _deleteRows(List deletedRows) { - if (deletedRows.isEmpty) { - return; - } - - final List newRows = []; - final DeletedIndexs deletedIndex = []; - final Map deletedRowByRowId = { - for (var rowId in deletedRows) rowId: rowId - }; - - _rowInfos.asMap().forEach((index, row) { - if (deletedRowByRowId[row.id] == null) { - newRows.add(row); - } else { - _rowByRowId.remove(row.id); - deletedIndex.add(DeletedIndex(index: index, row: row)); - } - }); - _rowInfos = newRows; - _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex)); - } - - void _insertRows(List insertRows) { - if (insertRows.isEmpty) { - return; - } - - InsertedIndexs insertIndexs = []; - for (final insertRow in insertRows) { - final insertIndex = InsertedIndex( - index: insertRow.index, - rowId: insertRow.rowId, - ); - insertIndexs.add(insertIndex); - _rowInfos.insert(insertRow.index, - (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); - } - - _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs)); - } - - void _updateRows(List updatedRows) { - if (updatedRows.isEmpty) { - return; - } - - final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - for (final updatedRow in updatedRows) { - final rowId = updatedRow.rowId; - final index = _rowInfos.indexWhere((row) => row.id == rowId); - if (index != -1) { - _rowByRowId[rowId] = updatedRow.row; - - _rowInfos.removeAt(index); - _rowInfos.insert( - index, buildGridRow(rowId, updatedRow.row.height.toDouble())); - updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); - } - } - - _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs)); - } - - void _hideRows(List hideRows) {} - - void _showRows(List visibleRows) {} - - void onRowsChanged( - void Function(GridRowChangeReason) onRowChanged, - ) { - _rowChangeReasonNotifier.addListener(() { - onRowChanged(_rowChangeReasonNotifier.reason); - }); - } - - RowUpdateCallback addListener({ - required String rowId, - void Function(GridCellMap, GridRowChangeReason)? onCellUpdated, - bool Function()? listenWhen, - }) { - listenerHandler() async { - if (listenWhen != null && listenWhen() == false) { - return; - } - - notifyUpdate() { - if (onCellUpdated != null) { - final row = _rowByRowId[rowId]; - if (row != null) { - final GridCellMap cellDataMap = _makeGridCells(rowId, row); - onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); - } - } - } - - _rowChangeReasonNotifier.reason.whenOrNull( - update: (indexs) { - if (indexs[rowId] != null) notifyUpdate(); - }, - fieldDidChange: () => notifyUpdate(), - ); - } - - _rowChangeReasonNotifier.addListener(listenerHandler); - return listenerHandler; - } - - void removeRowListener(VoidCallback callback) { - _rowChangeReasonNotifier.removeListener(callback); - } - - GridCellMap loadGridCells(String rowId) { - final GridRowPB? data = _rowByRowId[rowId]; - if (data == null) { - _loadRow(rowId); - } - return _makeGridCells(rowId, data); - } - - Future _loadRow(String rowId) async { - final payload = GridRowIdPB.create() - ..gridId = gridId - ..blockId = block.id - ..rowId = rowId; - - final result = await GridEventGetRow(payload).send(); - result.fold( - (optionRow) => _refreshRow(optionRow), - (err) => Log.error(err), - ); - } - - GridCellMap _makeGridCells(String rowId, GridRowPB? row) { - var cellDataMap = GridCellMap.new(); - for (final field in _fieldNotifier.fields) { - if (field.visibility) { - cellDataMap[field.id] = GridCellIdentifier( - rowId: rowId, - gridId: gridId, - field: field, - ); - } - } - return cellDataMap; - } - - void _refreshRow(OptionalRowPB optionRow) { - if (!optionRow.hasRow()) { - return; - } - final updatedRow = optionRow.row; - updatedRow.freeze(); - - _rowByRowId[updatedRow.id] = updatedRow; - final index = - _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id); - if (index != -1) { - // update the corresponding row in _rows if they are not the same - if (_rowInfos[index].rawRow != updatedRow) { - final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow); - _rowInfos.insert(index, row); - - // Calculate the update index - final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id); - - // - _rowChangeReasonNotifier - .receive(GridRowChangeReason.update(updatedIndexs)); - } - } - } - - GridRowInfo buildGridRow(String rowId, double rowHeight) { - return GridRowInfo( - gridId: gridId, - blockId: block.id, - fields: _fieldNotifier.fields, - id: rowId, - height: rowHeight, - ); - } -} - -class _GridRowChangesetNotifier extends ChangeNotifier { - GridRowChangeReason reason = const InitialListState(); - - _GridRowChangesetNotifier(); - - void receive(GridRowChangeReason newReason) { - reason = newReason; - reason.map( - insert: (_) => notifyListeners(), - delete: (_) => notifyListeners(), - update: (_) => notifyListeners(), - fieldDidChange: (_) => notifyListeners(), - initial: (_) {}, - ); - } -} class RowService { final String gridId; @@ -334,55 +60,3 @@ class RowService { return GridEventDuplicateRow(payload).send(); } } - -@freezed -class GridRowInfo with _$GridRowInfo { - const factory GridRowInfo({ - required String gridId, - required String blockId, - required String id, - required UnmodifiableListView fields, - required double height, - GridRowPB? rawRow, - }) = _GridRowInfo; -} - -typedef InsertedIndexs = List; -typedef DeletedIndexs = List; -typedef UpdatedIndexs = LinkedHashMap; - -@freezed -class GridRowChangeReason with _$GridRowChangeReason { - const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert; - const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete; - const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update; - const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange; - const factory GridRowChangeReason.initial() = InitialListState; -} - -class InsertedIndex { - final int index; - final String rowId; - InsertedIndex({ - required this.index, - required this.rowId, - }); -} - -class DeletedIndex { - final int index; - final GridRowInfo row; - DeletedIndex({ - required this.index, - required this.row, - }); -} - -class UpdatedIndex { - final int index; - final String rowId; - UpdatedIndex({ - required this.index, - required this.rowId, - }); -} diff --git a/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart index 7c185279ae..972b64f69a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart @@ -1,11 +1,12 @@ import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; +import '../field/field_cache.dart'; + part 'property_bloc.freezed.dart'; class GridPropertyBloc extends Bloc { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index d267e4aca9..c4fbbf4e92 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -1,7 +1,6 @@ import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; -import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; @@ -12,6 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; +import '../application/row/row_cache.dart'; import 'controller/grid_scroll.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart index c53bee8423..1220d39a9d 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart @@ -1,5 +1,5 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart index d334cc38af..ac80303f0d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart @@ -22,8 +22,8 @@ class _CheckboxCellState extends GridCellState { @override void initState() { - final cellContext = widget.cellControllerBuilder.build(); - _cellBloc = getIt(param1: cellContext) + final cellController = widget.cellControllerBuilder.build(); + _cellBloc = getIt(param1: cellController) ..add(const CheckboxCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart index 3254f58e6c..94e8ceeb71 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart @@ -43,8 +43,8 @@ class _DateCellState extends GridCellState { @override void initState() { - final cellContext = widget.cellControllerBuilder.build(); - _cellBloc = getIt(param1: cellContext) + final cellController = widget.cellControllerBuilder.build(); + _cellBloc = getIt(param1: cellController) ..add(const DateCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart index a24243ef99..004c34b657 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart @@ -25,8 +25,8 @@ class _NumberCellState extends GridFocusNodeCellState { @override void initState() { - final cellContext = widget.cellControllerBuilder.build(); - _cellBloc = getIt(param1: cellContext) + final cellController = widget.cellControllerBuilder.build(); + _cellBloc = getIt(param1: cellController) ..add(const NumberCellEvent.initial()); _controller = TextEditingController(text: contentFromState(_cellBloc.state)); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index f4ebfa8d4d..a4e09ec0b9 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -46,9 +46,9 @@ class _SingleSelectCellState extends State { @override void initState() { - final cellContext = + final cellController = widget.cellControllerBuilder.build() as GridSelectOptionCellController; - _cellBloc = getIt(param1: cellContext) + _cellBloc = getIt(param1: cellController) ..add(const SelectOptionCellEvent.initial()); super.initState(); } @@ -102,9 +102,9 @@ class _MultiSelectCellState extends State { @override void initState() { - final cellContext = + final cellController = widget.cellControllerBuilder.build() as GridSelectOptionCellController; - _cellBloc = getIt(param1: cellContext) + _cellBloc = getIt(param1: cellController) ..add(const SelectOptionCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart index 21f9c60631..04be48b9ea 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/text_cell.dart @@ -39,8 +39,8 @@ class _GridTextCellState extends GridFocusNodeCellState { @override void initState() { - final cellContext = widget.cellControllerBuilder.build(); - _cellBloc = getIt(param1: cellContext); + final cellController = widget.cellControllerBuilder.build(); + _cellBloc = getIt(param1: cellController); _cellBloc.add(const TextCellEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); super.initState(); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index 506cc3bc2b..7295228daf 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -52,10 +52,10 @@ class GridURLCell extends GridCellWidget { GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) { switch (ty) { case GridURLCellAccessoryType.edit: - final cellContext = + final cellController = cellControllerBuilder.build() as GridURLCellController; return _EditURLAccessory( - cellContext: cellContext, + cellContext: cellController, anchorContext: buildContext.anchorContext); case GridURLCellAccessoryType.copyURL: diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart index df7be4ee2b..caa40b2e7a 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart @@ -1,3 +1,4 @@ +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index d37a45d210..7ea59ab7e9 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart index 67c7b4893b..9c31bab5f3 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart @@ -1,5 +1,4 @@ import 'package:app_flowy/plugins/grid/application/row/row_action_sheet_bloc.dart'; -import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flowy_infra/image.dart'; @@ -12,6 +11,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/row/row_cache.dart'; import '../../layout/sizes.dart'; class GridRowActionSheet extends StatelessWidget { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index b45457fffd..6394b98874 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -1,7 +1,6 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart'; -import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -14,6 +13,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/row/row_cache.dart'; import '../../layout/sizes.dart'; import '../cell/cell_accessory.dart'; import '../cell/prelude.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index df4eb2115c..28f2b860ff 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart @@ -1,6 +1,5 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:app_flowy/plugins/grid/application/setting/property_bloc.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:flowy_infra/image.dart'; @@ -15,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; +import '../../../application/field/field_cache.dart'; import '../../layout/sizes.dart'; import '../header/field_editor.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart index d7c084fe2d..f555b6266a 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart @@ -1,4 +1,3 @@ -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; @@ -12,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; +import '../../../application/field/field_cache.dart'; import '../../layout/sizes.dart'; import 'grid_property.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart index 2f332ffe2f..4ced06bf99 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart @@ -1,4 +1,3 @@ -import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/extension.dart'; @@ -6,6 +5,7 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../application/field/field_cache.dart'; import '../../layout/sizes.dart'; import 'grid_setting.dart'; diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 8b173a4002..7062a9f3a8 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -21,6 +21,8 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get_it/get_it.dart'; +import '../plugins/grid/application/field/field_cache.dart'; + class DependencyResolver { static Future resolve(GetIt getIt) async { _resolveUserDeps(getIt); From 9c495957dcaf50d3fa26edb3957d9e6798794078 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 9 Aug 2022 20:19:43 +0800 Subject: [PATCH 041/224] refactor: RowDetailBloc --- .../cell/cell_service/context_builder.dart | 13 ++-- .../application/row/row_data_controller.dart | 32 ++++++--- .../grid/application/row/row_detail_bloc.dart | 39 +++++------ .../plugins/grid/presentation/grid_page.dart | 67 ++++++++++++++----- .../widgets/cell/cell_builder.dart | 24 ++++--- .../presentation/widgets/row/grid_row.dart | 54 +++++++-------- .../presentation/widgets/row/row_detail.dart | 11 ++- 7 files changed, 133 insertions(+), 107 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 12cadcca40..09564b46b0 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -7,23 +7,24 @@ typedef GridDateCellController = IGridCellController; typedef GridURLCellController = IGridCellController; +abstract class GridCellControllerBuilderDelegate { + GridCellFieldNotifier buildFieldNotifier(); +} + class GridCellControllerBuilder { final GridCellIdentifier _cellId; final GridCellCache _cellCache; - final GridFieldCache _fieldCache; + final GridCellControllerBuilderDelegate delegate; GridCellControllerBuilder({ + required this.delegate, required GridCellIdentifier cellId, required GridCellCache cellCache, - required GridFieldCache fieldCache, }) : _cellCache = cellCache, - _fieldCache = fieldCache, _cellId = cellId; IGridCellController build() { - final cellFieldNotifier = - GridCellFieldNotifier(notifier: GridCellFieldNotifierImpl(_fieldCache)); - + final cellFieldNotifier = delegate.buildFieldNotifier(); switch (_cellId.fieldType) { case FieldType.Checkbox: final cellDataLoader = GridCellDataLoader( diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart index 6975bd6a51..3f25e414f1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -1,13 +1,15 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart'; import 'package:flutter/material.dart'; +import '../../presentation/widgets/cell/cell_builder.dart'; import '../cell/cell_service/cell_service.dart'; import '../field/field_cache.dart'; import 'row_cache.dart'; typedef OnRowChanged = void Function(GridCellMap, GridRowChangeReason); -class GridRowDataController { - final String rowId; - VoidCallback? _onRowChangedListener; +class GridRowDataController extends GridCellBuilderDelegate { + final GridRowInfo rowInfo; + final List _onRowChangedListeners = []; final GridFieldCache _fieldCache; final GridRowCache _rowCache; @@ -16,26 +18,36 @@ class GridRowDataController { GridRowCache get rowCache => _rowCache; GridRowDataController({ - required this.rowId, + required this.rowInfo, required GridFieldCache fieldCache, required GridRowCache rowCache, }) : _fieldCache = fieldCache, _rowCache = rowCache; GridCellMap loadData() { - return _rowCache.loadGridCells(rowId); + return _rowCache.loadGridCells(rowInfo.id); } void addListener({OnRowChanged? onRowChanged}) { - _onRowChangedListener = _rowCache.addListener( - rowId: rowId, + _onRowChangedListeners.add(_rowCache.addListener( + rowId: rowInfo.id, onCellUpdated: onRowChanged, - ); + )); } void dispose() { - if (_onRowChangedListener != null) { - _rowCache.removeRowListener(_onRowChangedListener!); + for (final fn in _onRowChangedListeners) { + _rowCache.removeRowListener(fn); } } + + // GridCellBuilderDelegate implementation + @override + GridCellFieldNotifier buildFieldNotifier() { + return GridCellFieldNotifier( + notifier: GridCellFieldNotifierImpl(_fieldCache)); + } + + @override + GridCellCache get cellCache => rowCache.cellCache; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart index 17f6ef4990..6f59682f4d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_detail_bloc.dart @@ -2,25 +2,24 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; -import 'row_cache.dart'; +import 'row_data_controller.dart'; part 'row_detail_bloc.freezed.dart'; class RowDetailBloc extends Bloc { - final GridRowInfo rowInfo; - final GridRowCache _rowCache; - void Function()? _rowListenFn; + final GridRowDataController dataController; RowDetailBloc({ - required this.rowInfo, - required GridRowCache rowCache, - }) : _rowCache = rowCache, - super(RowDetailState.initial()) { + required this.dataController, + }) : super(RowDetailState.initial()) { on( (event, emit) async { await event.map( initial: (_Initial value) async { await _startListening(); - _loadCellData(); + final cells = dataController.loadData(); + if (!isClosed) { + add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); + } }, didReceiveCellDatas: (_DidReceiveCellDatas value) { emit(state.copyWith(gridCells: value.gridCells)); @@ -32,27 +31,19 @@ class RowDetailBloc extends Bloc { @override Future close() async { - if (_rowListenFn != null) { - _rowCache.removeRowListener(_rowListenFn!); - } + dataController.dispose(); return super.close(); } Future _startListening() async { - _rowListenFn = _rowCache.addListener( - rowId: rowInfo.id, - onCellUpdated: (cellDatas, reason) => - add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())), - listenWhen: () => !isClosed, + dataController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(RowDetailEvent.didReceiveCellDatas(cells.values.toList())); + } + }, ); } - - Future _loadCellData() async { - final cellDataMap = _rowCache.loadGridCells(rowInfo.id); - if (!isClosed) { - add(RowDetailEvent.didReceiveCellDatas(cellDataMap.values.toList())); - } - } } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index c4fbbf4e92..3d3be83c79 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -1,3 +1,4 @@ +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; @@ -15,9 +16,11 @@ import '../application/row/row_cache.dart'; import 'controller/grid_scroll.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; +import 'widgets/cell/cell_builder.dart'; import 'widgets/row/grid_row.dart'; import 'widgets/footer/grid_footer.dart'; import 'widgets/header/grid_header.dart'; +import 'widgets/row/row_detail.dart'; import 'widgets/shortcuts.dart'; import 'widgets/toolbar/grid_toolbar.dart'; @@ -239,25 +242,53 @@ class _GridRowsState extends State<_GridRows> { final rowCache = context.read().getRowCache(rowInfo.blockId, rowInfo.id); - final fieldCache = context.read().dataController.fieldCache; - if (rowCache != null) { - final dataController = GridRowDataController( - rowId: rowInfo.id, - fieldCache: fieldCache, - rowCache: rowCache, - ); + /// Return placeholder widget if the rowCache is null. + if (rowCache == null) return const SizedBox(); - return SizeTransition( - sizeFactor: animation, - child: GridRowWidget( - rowData: rowInfo, - dataController: dataController, - key: ValueKey(rowInfo.id), - ), - ); - } else { - return const SizedBox(); - } + final fieldCache = context.read().dataController.fieldCache; + final dataController = GridRowDataController( + rowInfo: rowInfo, + fieldCache: fieldCache, + rowCache: rowCache, + ); + + return SizeTransition( + sizeFactor: animation, + child: GridRowWidget( + rowInfo: rowInfo, + dataController: dataController, + cellBuilder: GridCellBuilder(delegate: dataController), + openDetailPage: (context, cellBuilder) { + _openRowDetailPage( + context, + rowInfo, + fieldCache, + rowCache, + cellBuilder, + ); + }, + key: ValueKey(rowInfo.id), + ), + ); + } + + void _openRowDetailPage( + BuildContext context, + GridRowInfo rowInfo, + GridFieldCache fieldCache, + GridRowCache rowCache, + GridCellBuilder cellBuilder, + ) { + final dataController = GridRowDataController( + rowInfo: rowInfo, + fieldCache: fieldCache, + rowCache: rowCache, + ); + + RowDetailPage( + cellBuilder: cellBuilder, + dataController: dataController, + ).show(context); } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart index 1220d39a9d..6c3fa38bc1 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart @@ -1,5 +1,4 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; -import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -13,23 +12,26 @@ import 'select_option_cell/select_option_cell.dart'; import 'text_cell.dart'; import 'url_cell/url_cell.dart'; +abstract class GridCellBuilderDelegate + extends GridCellControllerBuilderDelegate { + GridCellCache get cellCache; +} + class GridCellBuilder { - final GridCellCache cellCache; - final GridFieldCache fieldCache; + final GridCellBuilderDelegate delegate; GridCellBuilder({ - required this.cellCache, - required this.fieldCache, + required this.delegate, }); - GridCellWidget build(GridCellIdentifier cell, {GridCellStyle? style}) { + GridCellWidget build(GridCellIdentifier cellId, {GridCellStyle? style}) { final cellControllerBuilder = GridCellControllerBuilder( - cellId: cell, - cellCache: cellCache, - fieldCache: fieldCache, + cellId: cellId, + cellCache: delegate.cellCache, + delegate: delegate, ); - final key = cell.key(); - switch (cell.fieldType) { + final key = cellId.key(); + switch (cellId.fieldType) { case FieldType.Checkbox: return GridCheckboxCell( cellControllerBuilder: cellControllerBuilder, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 7ea59ab7e9..044ddbb5ee 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -14,22 +14,20 @@ import '../cell/cell_accessory.dart'; import '../cell/cell_container.dart'; import '../cell/prelude.dart'; import 'row_action_sheet.dart'; -import 'row_detail.dart'; class GridRowWidget extends StatefulWidget { - final GridRowInfo rowData; + final GridRowInfo rowInfo; final GridRowDataController dataController; final GridCellBuilder cellBuilder; + final void Function(BuildContext, GridCellBuilder) openDetailPage; - GridRowWidget({ - required this.rowData, + const GridRowWidget({ + required this.rowInfo, required this.dataController, + required this.cellBuilder, + required this.openDetailPage, Key? key, - }) : cellBuilder = GridCellBuilder( - cellCache: dataController.rowCache.cellCache, - fieldCache: dataController.fieldCache, - ), - super(key: key); + }) : super(key: key); @override State createState() => _GridRowWidgetState(); @@ -41,7 +39,7 @@ class _GridRowWidgetState extends State { @override void initState() { _rowBloc = RowBloc( - rowInfo: widget.rowData, + rowInfo: widget.rowInfo, dataController: widget.dataController, ); _rowBloc.add(const RowEvent.initial()); @@ -56,17 +54,20 @@ class _GridRowWidgetState extends State { child: BlocBuilder( buildWhen: (p, c) => p.rowInfo.height != c.rowInfo.height, builder: (context, state) { - return Row( - children: [ - const _RowLeading(), - Expanded( - child: _RowCells( + final children = [ + const _RowLeading(), + Expanded( + child: RowContent( builder: widget.cellBuilder, - onExpand: () => _expandRow(context), - )), - const _RowTrailing(), - ], - ); + onExpand: () => widget.openDetailPage( + context, + widget.cellBuilder, + ), + ), + ), + const _RowTrailing(), + ]; + return Row(children: children); }, ), ), @@ -78,15 +79,6 @@ class _GridRowWidgetState extends State { _rowBloc.close(); super.dispose(); } - - void _expandRow(BuildContext context) { - final page = RowDetailPage( - rowInfo: widget.rowData, - rowCache: widget.dataController.rowCache, - cellBuilder: widget.cellBuilder, - ); - page.show(context); - } } class _RowLeading extends StatelessWidget { @@ -159,10 +151,10 @@ class _DeleteRowButton extends StatelessWidget { } } -class _RowCells extends StatelessWidget { +class RowContent extends StatelessWidget { final VoidCallback onExpand; final GridCellBuilder builder; - const _RowCells({ + const RowContent({ required this.builder, required this.onExpand, Key? key, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index 6394b98874..bfaef332a9 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; @@ -13,7 +14,6 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../application/row/row_cache.dart'; import '../../layout/sizes.dart'; import '../cell/cell_accessory.dart'; import '../cell/prelude.dart'; @@ -21,13 +21,11 @@ import '../header/field_cell.dart'; import '../header/field_editor.dart'; class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { - final GridRowInfo rowInfo; - final GridRowCache rowCache; + final GridRowDataController dataController; final GridCellBuilder cellBuilder; const RowDetailPage({ - required this.rowInfo, - required this.rowCache, + required this.dataController, required this.cellBuilder, Key? key, }) : super(key: key); @@ -63,8 +61,7 @@ class _RowDetailPageState extends State { return BlocProvider( create: (context) { final bloc = RowDetailBloc( - rowInfo: widget.rowInfo, - rowCache: widget.rowCache, + dataController: widget.dataController, ); bloc.add(const RowDetailEvent.initial()); return bloc; From 25eb5bf1b0eb319345cc9181c8e684b3b8da31dd Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 9 Aug 2022 20:37:01 +0800 Subject: [PATCH 042/224] chore: add boardbloc --- .../plugins/board/application/board_bloc.dart | 161 ++++++++++++++++++ .../board/presentation/board_page.dart | 39 +++++ 2 files changed, 200 insertions(+) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index e69de29bb2..6d5eeace4c 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; +import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:collection'; + +part 'board_bloc.freezed.dart'; + +class BoardBloc extends Bloc { + final GridDataController dataController; + + BoardBloc({required ViewPB view}) + : dataController = GridDataController(view: view), + super(BoardState.initial(view.id)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + await _loadGrid(emit); + }, + createRow: () { + dataController.createRow(); + }, + didReceiveGridUpdate: (grid) { + emit(state.copyWith(grid: Some(grid))); + }, + didReceiveFieldUpdate: (fields) { + emit(state.copyWith( + fields: GridFieldEquatable(fields), + )); + }, + didReceiveRowUpdate: (newRowInfos, reason) { + emit(state.copyWith( + rowInfos: newRowInfos, + reason: reason, + )); + }, + ); + }, + ); + } + + @override + Future close() async { + await dataController.dispose(); + return super.close(); + } + + GridRowCache? getRowCache(String blockId, String rowId) { + final GridBlockCache? blockCache = dataController.blocks[blockId]; + return blockCache?.rowCache; + } + + void _startListening() { + dataController.addListener( + onGridChanged: (grid) { + if (!isClosed) { + add(BoardEvent.didReceiveGridUpdate(grid)); + } + }, + onRowsChanged: (rowInfos, reason) { + if (!isClosed) { + add(BoardEvent.didReceiveRowUpdate(rowInfos, reason)); + } + }, + onFieldsChanged: (fields) { + if (!isClosed) { + add(BoardEvent.didReceiveFieldUpdate(fields)); + } + }, + ); + } + + Future _loadGrid(Emitter emit) async { + final result = await dataController.loadData(); + result.fold( + (grid) => emit( + state.copyWith(loadingState: GridLoadingState.finish(left(unit))), + ), + (err) => emit( + state.copyWith(loadingState: GridLoadingState.finish(right(err))), + ), + ); + } +} + +@freezed +class BoardEvent with _$BoardEvent { + const factory BoardEvent.initial() = InitialGrid; + const factory BoardEvent.createRow() = _CreateRow; + const factory BoardEvent.didReceiveRowUpdate( + List rows, + GridRowChangeReason listState, + ) = _DidReceiveRowUpdate; + const factory BoardEvent.didReceiveFieldUpdate( + UnmodifiableListView fields, + ) = _DidReceiveFieldUpdate; + + const factory BoardEvent.didReceiveGridUpdate( + GridPB grid, + ) = _DidReceiveGridUpdate; +} + +@freezed +class BoardState with _$BoardState { + const factory BoardState({ + required String gridId, + required Option grid, + required GridFieldEquatable fields, + required List rowInfos, + required GridLoadingState loadingState, + required GridRowChangeReason reason, + }) = _BoardState; + + factory BoardState.initial(String gridId) => BoardState( + fields: GridFieldEquatable(UnmodifiableListView([])), + rowInfos: [], + grid: none(), + gridId: gridId, + loadingState: const _Loading(), + reason: const InitialListState(), + ); +} + +@freezed +class GridLoadingState with _$GridLoadingState { + const factory GridLoadingState.loading() = _Loading; + const factory GridLoadingState.finish( + Either successOrFail) = _Finish; +} + +class GridFieldEquatable extends Equatable { + final UnmodifiableListView _fields; + const GridFieldEquatable( + UnmodifiableListView fields, + ) : _fields = fields; + + @override + List get props { + if (_fields.isEmpty) { + return []; + } + + return [ + _fields.length, + _fields + .map((field) => field.width) + .reduce((value, element) => value + element), + ]; + } + + UnmodifiableListView get value => UnmodifiableListView(_fields); +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 953587852a..78d93b8e30 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,8 +1,47 @@ // ignore_for_file: unused_field import 'package:appflowy_board/appflowy_board.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../application/board_bloc.dart'; + +class BoardPage2 extends StatelessWidget { + final ViewPB view; + const BoardPage2({required this.view, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => BoardBloc(view: view), + child: BlocBuilder( + builder: (context, state) { + return state.loadingState.map( + loading: (_) => + const Center(child: CircularProgressIndicator.adaptive()), + finish: (result) { + return result.successOrFail.fold( + (_) => const BoardContent(), + (err) => FlowyErrorPage(err.toString()), + ); + }, + ); + }, + ), + ); + } +} + +class BoardContent extends StatelessWidget { + const BoardContent({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} class BoardPage extends StatefulWidget { final ViewPB _view; From 0d6c04ae8132dcf8bea703b26a36bd4ecf531b35 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 12:58:07 +0800 Subject: [PATCH 043/224] chore: load field typeOption --- .../plugins/board/application/board_bloc.dart | 102 +++++- .../board/presentation/board_page.dart | 164 +++------ .../grid/application/field/field_cache.dart | 192 ++++++++++ .../application/field/field_editor_bloc.dart | 8 +- .../grid/application/field/field_service.dart | 8 +- .../type_option/multi_select_type_option.dart | 4 +- .../single_select_type_option.dart | 8 +- .../grid/application/row/row_cache.dart | 329 ++++++++++++++++++ .../widgets/header/type_option/builder.dart | 4 +- .../appflowy_board/lib/src/utils/log.dart | 6 + .../board_column/board_column_data.dart | 26 +- .../lib/src/widgets/board_data.dart | 66 +++- 12 files changed, 759 insertions(+), 158 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart create mode 100644 frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 6d5eeace4c..1d73e247d0 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; @@ -14,11 +15,32 @@ import 'dart:collection'; part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { - final GridDataController dataController; + final GridDataController _gridDataController; + late final BoardDataController boardDataController; BoardBloc({required ViewPB view}) - : dataController = GridDataController(view: view), + : _gridDataController = GridDataController(view: view), super(BoardState.initial(view.id)) { + boardDataController = BoardDataController( + onMoveColumn: ( + fromIndex, + toIndex, + ) {}, + onMoveColumnItem: ( + columnId, + fromIndex, + toIndex, + ) {}, + onMoveColumnItemToColumn: ( + fromColumnId, + fromIndex, + toColumnId, + toIndex, + ) {}, + ); + + // boardDataController.addColumns(_buildColumns()); + on( (event, emit) async { await event.when( @@ -27,21 +49,19 @@ class BoardBloc extends Bloc { await _loadGrid(emit); }, createRow: () { - dataController.createRow(); + _gridDataController.createRow(); }, - didReceiveGridUpdate: (grid) { + didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - didReceiveFieldUpdate: (fields) { - emit(state.copyWith( - fields: GridFieldEquatable(fields), - )); + didReceiveFieldUpdate: (UnmodifiableListView fields) { + emit(state.copyWith(fields: GridFieldEquatable(fields))); }, - didReceiveRowUpdate: (newRowInfos, reason) { - emit(state.copyWith( - rowInfos: newRowInfos, - reason: reason, - )); + didReceiveRowUpdate: ( + List newRowInfos, + GridRowChangeReason reason, + ) { + emit(state.copyWith(rowInfos: newRowInfos, reason: reason)); }, ); }, @@ -50,17 +70,17 @@ class BoardBloc extends Bloc { @override Future close() async { - await dataController.dispose(); + await _gridDataController.dispose(); return super.close(); } GridRowCache? getRowCache(String blockId, String rowId) { - final GridBlockCache? blockCache = dataController.blocks[blockId]; + final GridBlockCache? blockCache = _gridDataController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - dataController.addListener( + _gridDataController.addListener( onGridChanged: (grid) { if (!isClosed) { add(BoardEvent.didReceiveGridUpdate(grid)); @@ -73,14 +93,43 @@ class BoardBloc extends Bloc { }, onFieldsChanged: (fields) { if (!isClosed) { + _buildColumns(fields); add(BoardEvent.didReceiveFieldUpdate(fields)); } }, ); } + void _buildColumns(UnmodifiableListView fields) { + List columns = []; + + for (final field in fields) { + if (field.fieldType == FieldType.SingleSelect) { + // return BoardColumnData(customData: field, id: field.id, desc: "1"); + } + } + + boardDataController.addColumns(columns); + + // final column1 = BoardColumnData(id: "To Do", items: [ + // TextItem("Card 1"), + // TextItem("Card 2"), + // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 4"), + // ]); + // final column2 = BoardColumnData(id: "In Progress", items: [ + // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 6"), + // ]); + + // final column3 = BoardColumnData(id: "Done", items: []); + // boardDataController.addColumn(column1); + // boardDataController.addColumn(column2); + // boardDataController.addColumn(column3); + } + Future _loadGrid(Emitter emit) async { - final result = await dataController.loadData(); + final result = await _gridDataController.loadData(); result.fold( (grid) => emit( state.copyWith(loadingState: GridLoadingState.finish(left(unit))), @@ -159,3 +208,22 @@ class GridFieldEquatable extends Equatable { UnmodifiableListView get value => UnmodifiableListView(_fields); } + +class TextItem extends ColumnItem { + final String s; + + TextItem(this.s); + + @override + String get id => s; +} + +class RichTextItem extends ColumnItem { + final String title; + final String subtitle; + + RichTextItem({required this.title, required this.subtitle}); + + @override + String get id => title; +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 78d93b8e30..570c1207df 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -3,19 +3,20 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import '../application/board_bloc.dart'; -class BoardPage2 extends StatelessWidget { +class BoardPage extends StatelessWidget { final ViewPB view; - const BoardPage2({required this.view, Key? key}) : super(key: key); + const BoardPage({required this.view, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => BoardBloc(view: view), + create: (context) => + BoardBloc(view: view)..add(const BoardEvent.initial()), child: BlocBuilder( builder: (context, state) { return state.loadingState.map( @@ -23,7 +24,7 @@ class BoardPage2 extends StatelessWidget { const Center(child: CircularProgressIndicator.adaptive()), finish: (result) { return result.successOrFail.fold( - (_) => const BoardContent(), + (_) => BoardContent(), (err) => FlowyErrorPage(err.toString()), ); }, @@ -35,100 +36,58 @@ class BoardPage2 extends StatelessWidget { } class BoardContent extends StatelessWidget { - const BoardContent({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container(); - } -} - -class BoardPage extends StatefulWidget { - final ViewPB _view; - - const BoardPage({required ViewPB view, Key? key}) - : _view = view, - super(key: key); - - @override - State createState() => _BoardPageState(); -} - -class _BoardPageState extends State { - final BoardDataController boardDataController = BoardDataController( - onMoveColumn: (fromIndex, toIndex) { - debugPrint('Move column from $fromIndex to $toIndex'); - }, - onMoveColumnItem: (columnId, fromIndex, toIndex) { - debugPrint('Move $columnId:$fromIndex to $columnId:$toIndex'); - }, - onMoveColumnItemToColumn: (fromColumnId, fromIndex, toColumnId, toIndex) { - debugPrint('Move $fromColumnId:$fromIndex to $toColumnId:$toIndex'); - }, + final config = BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); - @override - void initState() { - final column1 = BoardColumnData(id: "To Do", items: [ - TextItem("Card 1"), - TextItem("Card 2"), - RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 4"), - ]); - final column2 = BoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 6"), - ]); - - final column3 = BoardColumnData(id: "Done", items: []); - - boardDataController.addColumn(column1); - boardDataController.addColumn(column2); - boardDataController.addColumn(column3); - super.initState(); - } + BoardContent({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final config = BoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), - ); - return Container( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Board( - dataController: boardDataController, - footBuilder: (context, columnData) { - return AppFlowyColumnFooter( - icon: const Icon(Icons.add, size: 20), - title: const Text('New'), - height: 50, - margin: config.columnItemPadding, - ); - }, - headerBuilder: (context, columnData) { - return AppFlowyColumnHeader( - icon: const Icon(Icons.lightbulb_circle), - title: Text(columnData.id), - addIcon: const Icon(Icons.add, size: 20), - moreIcon: const Icon(Icons.more_horiz, size: 20), - height: 50, - margin: config.columnItemPadding, - ); - }, - cardBuilder: (context, item) { - return AppFlowyColumnItemCard( - key: ObjectKey(item), - child: _buildCard(item), - ); - }, - columnConstraints: const BoxConstraints.tightFor(width: 240), - config: BoardConfig( - columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + return BlocBuilder( + builder: (context, state) { + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Board( + dataController: context.read().boardDataController, + headerBuilder: _buildHeader, + footBuilder: _buildFooter, + cardBuilder: (context, item) { + return AppFlowyColumnItemCard( + key: ObjectKey(item), + child: _buildCard(item), + ); + }, + columnConstraints: const BoxConstraints.tightFor(width: 240), + config: BoardConfig( + columnBackgroundColor: HexColor.fromHex('#F7F8FC'), + ), + ), ), - ), - ), + ); + }, + ); + } + + Widget _buildHeader(BuildContext context, BoardColumnData columnData) { + return AppFlowyColumnHeader( + icon: const Icon(Icons.lightbulb_circle), + title: Text(columnData.desc), + addIcon: const Icon(Icons.add, size: 20), + moreIcon: const Icon(Icons.more_horiz, size: 20), + height: 50, + margin: config.columnItemPadding, + ); + } + + Widget _buildFooter(BuildContext context, BoardColumnData columnData) { + return AppFlowyColumnFooter( + icon: const Icon(Icons.add, size: 20), + title: const Text('New'), + height: 50, + margin: config.columnItemPadding, ); } @@ -171,25 +130,6 @@ class _BoardPageState extends State { } } -class TextItem extends ColumnItem { - final String s; - - TextItem(this.s); - - @override - String get id => s; -} - -class RichTextItem extends ColumnItem { - final String title; - final String subtitle; - - RichTextItem({required this.title, required this.subtitle}); - - @override - String get id => title; -} - extension HexColor on Color { static Color fromHex(String hexString) { final buffer = StringBuffer(); diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart new file mode 100644 index 0000000000..9597c871c1 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart @@ -0,0 +1,192 @@ +import 'dart:collection'; + +import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/foundation.dart'; + +import '../row/row_cache.dart'; + +class FieldsNotifier extends ChangeNotifier { + List _fields = []; + + set fields(List fields) { + _fields = fields; + notifyListeners(); + } + + List get fields => _fields; +} + +typedef FieldChangesetCallback = void Function(GridFieldChangesetPB); +typedef FieldsCallback = void Function(List); + +class GridFieldCache { + final String gridId; + final GridFieldsListener _fieldListener; + FieldsNotifier? _fieldNotifier = FieldsNotifier(); + final Map _fieldsCallbackMap = {}; + final Map + _changesetCallbackMap = {}; + + GridFieldCache({required this.gridId}) + : _fieldListener = GridFieldsListener(gridId: gridId) { + _fieldListener.start(onFieldsChanged: (result) { + result.fold( + (changeset) { + _deleteFields(changeset.deletedFields); + _insertFields(changeset.insertedFields); + _updateFields(changeset.updatedFields); + for (final listener in _changesetCallbackMap.values) { + listener(changeset); + } + }, + (err) => Log.error(err), + ); + }); + } + + Future dispose() async { + await _fieldListener.stop(); + _fieldNotifier?.dispose(); + _fieldNotifier = null; + } + + UnmodifiableListView get unmodifiableFields => + UnmodifiableListView(_fieldNotifier?.fields ?? []); + + List get fields => [..._fieldNotifier?.fields ?? []]; + + set fields(List fields) { + _fieldNotifier?.fields = [...fields]; + } + + void addListener({ + FieldsCallback? onFields, + FieldChangesetCallback? onChangeset, + bool Function()? listenWhen, + }) { + if (onChangeset != null) { + fn(c) { + if (listenWhen != null && listenWhen() == false) { + return; + } + onChangeset(c); + } + + _changesetCallbackMap[onChangeset] = fn; + } + + if (onFields != null) { + fn() { + if (listenWhen != null && listenWhen() == false) { + return; + } + onFields(fields); + } + + _fieldsCallbackMap[onFields] = fn; + _fieldNotifier?.addListener(fn); + } + } + + void removeListener({ + FieldsCallback? onFieldsListener, + FieldChangesetCallback? onChangesetListener, + }) { + if (onFieldsListener != null) { + final fn = _fieldsCallbackMap.remove(onFieldsListener); + if (fn != null) { + _fieldNotifier?.removeListener(fn); + } + } + + if (onChangesetListener != null) { + _changesetCallbackMap.remove(onChangesetListener); + } + } + + void _deleteFields(List deletedFields) { + if (deletedFields.isEmpty) { + return; + } + final List newFields = fields; + final Map deletedFieldMap = { + for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder + }; + + newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); + _fieldNotifier?.fields = newFields; + } + + void _insertFields(List insertedFields) { + if (insertedFields.isEmpty) { + return; + } + final List newFields = fields; + for (final indexField in insertedFields) { + if (newFields.length > indexField.index) { + newFields.insert(indexField.index, indexField.field_1); + } else { + newFields.add(indexField.field_1); + } + } + _fieldNotifier?.fields = newFields; + } + + void _updateFields(List updatedFields) { + if (updatedFields.isEmpty) { + return; + } + final List newFields = fields; + for (final updatedField in updatedFields) { + final index = + newFields.indexWhere((field) => field.id == updatedField.id); + if (index != -1) { + newFields.removeAt(index); + newFields.insert(index, updatedField); + } + } + _fieldNotifier?.fields = newFields; + } +} + +class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { + final GridFieldCache _cache; + FieldChangesetCallback? _onChangesetFn; + FieldsCallback? _onFieldFn; + GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache; + + @override + UnmodifiableListView get fields => _cache.unmodifiableFields; + + @override + void onRowFieldsChanged(VoidCallback callback) { + _onFieldFn = (_) => callback(); + _cache.addListener(onFields: _onFieldFn); + } + + @override + void onRowFieldChanged(void Function(GridFieldPB) callback) { + _onChangesetFn = (GridFieldChangesetPB changeset) { + for (final updatedField in changeset.updatedFields) { + callback(updatedField); + } + }; + + _cache.addListener(onChangeset: _onChangesetFn); + } + + @override + void onRowDispose() { + if (_onFieldFn != null) { + _cache.removeListener(onFieldsListener: _onFieldFn!); + _onFieldFn = null; + } + + if (_onChangesetFn != null) { + _cache.removeListener(onChangesetListener: _onChangesetFn!); + _onChangesetFn = null; + } + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart index 8d44edf1ff..57e04cbaf4 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart @@ -13,7 +13,8 @@ class FieldEditorBloc extends Bloc { required String gridId, required String fieldName, required IFieldTypeOptionLoader loader, - }) : dataController = TypeOptionDataController(gridId: gridId, loader: loader), + }) : dataController = + TypeOptionDataController(gridId: gridId, loader: loader), super(FieldEditorState.initial(gridId, fieldName)) { on( (event, emit) async { @@ -24,7 +25,7 @@ class FieldEditorBloc extends Bloc { add(FieldEditorEvent.didReceiveFieldChanged(field)); } }); - await dataController.loadData(); + await dataController.loadTypeOptionData(); }, updateName: (name) { dataController.fieldName = name; @@ -48,7 +49,8 @@ class FieldEditorBloc extends Bloc { class FieldEditorEvent with _$FieldEditorEvent { const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.updateName(String name) = _UpdateName; - const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = _DidReceiveFieldChanged; + const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = + _DidReceiveFieldChanged; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index 9274770b21..cf4f374a09 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -146,7 +146,8 @@ abstract class IFieldTypeOptionLoader { String get gridId; Future> load(); - Future> switchToField(String fieldId, FieldType fieldType) { + Future> switchToField( + String fieldId, FieldType fieldType) { final payload = EditFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId @@ -206,7 +207,7 @@ class TypeOptionDataController { required IFieldTypeOptionLoader loader, }) : _loader = loader; - Future> loadData() async { + Future> loadTypeOptionData() async { final result = await _loader.load(); return result.fold( (data) { @@ -238,7 +239,8 @@ class TypeOptionDataController { _updateData(newTypeOptionData: typeOptionData); } - void _updateData({String? newName, GridFieldPB? newField, List? newTypeOptionData}) { + void _updateData( + {String? newName, GridFieldPB? newField, List? newTypeOptionData}) { _data = _data.rebuild((rebuildData) { if (newName != null) { rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart index ebc88aaf95..2f7d2dbc5a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart @@ -13,13 +13,13 @@ class MultiSelectTypeOptionContext final TypeOptionService service; MultiSelectTypeOptionContext({ - required MultiSelectTypeOptionWidgetDataParser dataBuilder, + required MultiSelectTypeOptionWidgetDataParser dataParser, required TypeOptionDataController dataController, }) : service = TypeOptionService( gridId: dataController.gridId, fieldId: dataController.field.id, ), - super(dataParser: dataBuilder, dataController: dataController); + super(dataParser: dataParser, dataController: dataController); @override List Function(SelectOptionPB) get deleteOption { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart index bdf89b5b78..bc995a4237 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart @@ -14,12 +14,12 @@ class SingleSelectTypeOptionContext SingleSelectTypeOptionContext({ required SingleSelectTypeOptionWidgetDataParser dataBuilder, - required TypeOptionDataController fieldContext, + required TypeOptionDataController dataController, }) : service = TypeOptionService( - gridId: fieldContext.gridId, - fieldId: fieldContext.field.id, + gridId: dataController.gridId, + fieldId: dataController.field.id, ), - super(dataParser: dataBuilder, dataController: fieldContext); + super(dataParser: dataBuilder, dataController: dataController); @override List Function(SelectOptionPB) get deleteOption { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart new file mode 100644 index 0000000000..4f63794afc --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -0,0 +1,329 @@ +import 'dart:collection'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/dispatch/dispatch.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'row_cache.freezed.dart'; + +typedef RowUpdateCallback = void Function(); + +abstract class IGridRowFieldNotifier { + UnmodifiableListView get fields; + void onRowFieldsChanged(VoidCallback callback); + void onRowFieldChanged(void Function(GridFieldPB) callback); + void onRowDispose(); +} + +/// Cache the rows in memory +/// Insert / delete / update row +/// +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. + +class GridRowCache { + final String gridId; + final GridBlockPB block; + + /// _rows containers the current block's rows + /// Use List to reverse the order of the GridRow. + List _rowInfos = []; + + /// Use Map for faster access the raw row data. + final HashMap _rowByRowId; + + final GridCellCache _cellCache; + final IGridRowFieldNotifier _fieldNotifier; + final _GridRowChangesetNotifier _rowChangeReasonNotifier; + + UnmodifiableListView get rows => UnmodifiableListView(_rowInfos); + GridCellCache get cellCache => _cellCache; + + GridRowCache({ + required this.gridId, + required this.block, + required IGridRowFieldNotifier notifier, + }) : _cellCache = GridCellCache(gridId: gridId), + _rowByRowId = HashMap(), + _rowChangeReasonNotifier = _GridRowChangesetNotifier(), + _fieldNotifier = notifier { + // + notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier + .receive(const GridRowChangeReason.fieldDidChange())); + notifier.onRowFieldChanged((field) => _cellCache.remove(field.id)); + _rowInfos = block.rows + .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble())) + .toList(); + } + + Future dispose() async { + _fieldNotifier.onRowDispose(); + _rowChangeReasonNotifier.dispose(); + await _cellCache.dispose(); + } + + void applyChangesets(List changesets) { + for (final changeset in changesets) { + _deleteRows(changeset.deletedRows); + _insertRows(changeset.insertedRows); + _updateRows(changeset.updatedRows); + _hideRows(changeset.hideRows); + _showRows(changeset.visibleRows); + } + } + + void _deleteRows(List deletedRows) { + if (deletedRows.isEmpty) { + return; + } + + final List newRows = []; + final DeletedIndexs deletedIndex = []; + final Map deletedRowByRowId = { + for (var rowId in deletedRows) rowId: rowId + }; + + _rowInfos.asMap().forEach((index, row) { + if (deletedRowByRowId[row.id] == null) { + newRows.add(row); + } else { + _rowByRowId.remove(row.id); + deletedIndex.add(DeletedIndex(index: index, row: row)); + } + }); + _rowInfos = newRows; + _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex)); + } + + void _insertRows(List insertRows) { + if (insertRows.isEmpty) { + return; + } + + InsertedIndexs insertIndexs = []; + for (final insertRow in insertRows) { + final insertIndex = InsertedIndex( + index: insertRow.index, + rowId: insertRow.rowId, + ); + insertIndexs.add(insertIndex); + _rowInfos.insert(insertRow.index, + (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); + } + + _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs)); + } + + void _updateRows(List updatedRows) { + if (updatedRows.isEmpty) { + return; + } + + final UpdatedIndexs updatedIndexs = UpdatedIndexs(); + for (final updatedRow in updatedRows) { + final rowId = updatedRow.rowId; + final index = _rowInfos.indexWhere((row) => row.id == rowId); + if (index != -1) { + _rowByRowId[rowId] = updatedRow.row; + + _rowInfos.removeAt(index); + _rowInfos.insert( + index, buildGridRow(rowId, updatedRow.row.height.toDouble())); + updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); + } + } + + _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs)); + } + + void _hideRows(List hideRows) {} + + void _showRows(List visibleRows) {} + + void onRowsChanged( + void Function(GridRowChangeReason) onRowChanged, + ) { + _rowChangeReasonNotifier.addListener(() { + onRowChanged(_rowChangeReasonNotifier.reason); + }); + } + + RowUpdateCallback addListener({ + required String rowId, + void Function(GridCellMap, GridRowChangeReason)? onCellUpdated, + bool Function()? listenWhen, + }) { + listenerHandler() async { + if (listenWhen != null && listenWhen() == false) { + return; + } + + notifyUpdate() { + if (onCellUpdated != null) { + final row = _rowByRowId[rowId]; + if (row != null) { + final GridCellMap cellDataMap = _makeGridCells(rowId, row); + onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); + } + } + } + + _rowChangeReasonNotifier.reason.whenOrNull( + update: (indexs) { + if (indexs[rowId] != null) notifyUpdate(); + }, + fieldDidChange: () => notifyUpdate(), + ); + } + + _rowChangeReasonNotifier.addListener(listenerHandler); + return listenerHandler; + } + + void removeRowListener(VoidCallback callback) { + _rowChangeReasonNotifier.removeListener(callback); + } + + GridCellMap loadGridCells(String rowId) { + final GridRowPB? data = _rowByRowId[rowId]; + if (data == null) { + _loadRow(rowId); + } + return _makeGridCells(rowId, data); + } + + Future _loadRow(String rowId) async { + final payload = GridRowIdPB.create() + ..gridId = gridId + ..blockId = block.id + ..rowId = rowId; + + final result = await GridEventGetRow(payload).send(); + result.fold( + (optionRow) => _refreshRow(optionRow), + (err) => Log.error(err), + ); + } + + GridCellMap _makeGridCells(String rowId, GridRowPB? row) { + var cellDataMap = GridCellMap.new(); + for (final field in _fieldNotifier.fields) { + if (field.visibility) { + cellDataMap[field.id] = GridCellIdentifier( + rowId: rowId, + gridId: gridId, + field: field, + ); + } + } + return cellDataMap; + } + + void _refreshRow(OptionalRowPB optionRow) { + if (!optionRow.hasRow()) { + return; + } + final updatedRow = optionRow.row; + updatedRow.freeze(); + + _rowByRowId[updatedRow.id] = updatedRow; + final index = + _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id); + if (index != -1) { + // update the corresponding row in _rows if they are not the same + if (_rowInfos[index].rawRow != updatedRow) { + final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow); + _rowInfos.insert(index, row); + + // Calculate the update index + final UpdatedIndexs updatedIndexs = UpdatedIndexs(); + updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id); + + // + _rowChangeReasonNotifier + .receive(GridRowChangeReason.update(updatedIndexs)); + } + } + } + + GridRowInfo buildGridRow(String rowId, double rowHeight) { + return GridRowInfo( + gridId: gridId, + blockId: block.id, + fields: _fieldNotifier.fields, + id: rowId, + height: rowHeight, + ); + } +} + +class _GridRowChangesetNotifier extends ChangeNotifier { + GridRowChangeReason reason = const InitialListState(); + + _GridRowChangesetNotifier(); + + void receive(GridRowChangeReason newReason) { + reason = newReason; + reason.map( + insert: (_) => notifyListeners(), + delete: (_) => notifyListeners(), + update: (_) => notifyListeners(), + fieldDidChange: (_) => notifyListeners(), + initial: (_) {}, + ); + } +} + +@freezed +class GridRowInfo with _$GridRowInfo { + const factory GridRowInfo({ + required String gridId, + required String blockId, + required String id, + required UnmodifiableListView fields, + required double height, + GridRowPB? rawRow, + }) = _GridRowInfo; +} + +typedef InsertedIndexs = List; +typedef DeletedIndexs = List; +typedef UpdatedIndexs = LinkedHashMap; + +@freezed +class GridRowChangeReason with _$GridRowChangeReason { + const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert; + const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete; + const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update; + const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange; + const factory GridRowChangeReason.initial() = InitialListState; +} + +class InsertedIndex { + final int index; + final String rowId; + InsertedIndex({ + required this.index, + required this.rowId, + }); +} + +class DeletedIndex { + final int index; + final GridRowInfo row; + DeletedIndex({ + required this.index, + required this.row, + }); +} + +class UpdatedIndex { + final int index; + final String rowId; + UpdatedIndex({ + required this.index, + required this.rowId, + }); +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 4471e672a3..787f2091a9 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -65,7 +65,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder( ); case FieldType.SingleSelect: final context = SingleSelectTypeOptionContext( - fieldContext: dataController, + dataController: dataController, dataBuilder: SingleSelectTypeOptionWidgetDataParser(), ); return SingleSelectTypeOptionWidgetBuilder( @@ -75,7 +75,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder( case FieldType.MultiSelect: final context = MultiSelectTypeOptionContext( dataController: dataController, - dataBuilder: MultiSelectTypeOptionWidgetDataParser(), + dataParser: MultiSelectTypeOptionWidgetDataParser(), ); return MultiSelectTypeOptionWidgetBuilder( context, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart index b9f766f961..d11b5fd263 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/utils/log.dart @@ -20,6 +20,12 @@ class Log { } } + static void warn(String? message) { + if (enableLog) { + debugPrint('🐛[Warn]=> $message'); + } + } + static void trace(String? message) { if (enableLog) { // debugPrint('❗️[Trace]=> $message'); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 2ce739220e..97551ef350 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -56,25 +56,26 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the /// [fromIndex] equal to the [toIndex]. - void move(int fromIndex, int toIndex) { + bool move(int fromIndex, int toIndex) { assert(fromIndex >= 0); assert(toIndex >= 0); if (fromIndex == toIndex) { - return; + return false; } Log.debug( '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex'); final item = columnData._items.removeAt(fromIndex); columnData._items.insert(toIndex, item); notifyListeners(); + return true; } /// Insert an item to [index] and notify the listen if the value of [notify] /// is true. /// /// The default value of [notify] is true. - void insert(int index, ColumnItem item, {bool notify = true}) { + bool insert(int index, ColumnItem item, {bool notify = true}) { assert(index >= 0); Log.debug( '[$BoardColumnDataController] $columnData insert $item at $index'); @@ -85,9 +86,14 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { columnData._items.add(item); } - if (notify) { - notifyListeners(); - } + if (notify) notifyListeners(); + return true; + } + + bool add(ColumnItem item, {bool notify = true}) { + columnData._items.add(item); + if (notify) notifyListeners(); + return true; } /// Replace the item at index with the [newItem]. @@ -107,14 +113,18 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { } /// [BoardColumnData] represents the data of each Column of the Board. -class BoardColumnData extends ReoderFlexItem with EquatableMixin { +class BoardColumnData extends ReoderFlexItem with EquatableMixin { @override final String id; + final String desc; final List _items; + final CustomData? customData; BoardColumnData({ + this.customData, required this.id, - required List items, + this.desc = "", + List items = const [], }) : _items = items; /// Returns the readonly List diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 06e8ff1a57..37bae68121 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -44,32 +44,84 @@ class BoardDataController extends ChangeNotifier this.onMoveColumnItemToColumn, }); - void addColumn(BoardColumnData columnData) { + void addColumn(BoardColumnData columnData, {bool notify = true}) { + if (_columnControllers[columnData.id] != null) return; + final controller = BoardColumnDataController(columnData: columnData); _columnDatas.add(columnData); _columnControllers[columnData.id] = controller; + if (notify) notifyListeners(); + } + + void addColumns(List columns, {bool notify = true}) { + for (final column in columns) { + addColumn(column, notify: false); + } + + if (columns.isNotEmpty && notify) notifyListeners(); + } + + void removeColumn(String columnId, {bool notify = true}) { + final index = _columnDatas.indexWhere((column) => column.id == columnId); + if (index == -1) { + Log.warn( + 'Try to remove Column:[$columnId] failed. Column:[$columnId] not exist'); + } + + if (index != -1) { + _columnDatas.removeAt(index); + _columnControllers.remove(columnId); + + if (notify) notifyListeners(); + } + } + + void removeColumns(List columnIds, {bool notify = true}) { + for (final columnId in columnIds) { + removeColumn(columnId, notify: false); + } + + if (columnIds.isNotEmpty && notify) notifyListeners(); } BoardColumnDataController columnController(String columnId) { return _columnControllers[columnId]!; } - void moveColumn(int fromIndex, int toIndex) { + BoardColumnDataController? getColumnController(String columnId) { + final columnController = _columnControllers[columnId]; + if (columnController == null) { + Log.warn('Column:[$columnId] \'s controller is not exist'); + } + + return columnController; + } + + void moveColumn(int fromIndex, int toIndex, {bool notify = true}) { final columnData = _columnDatas.removeAt(fromIndex); _columnDatas.insert(toIndex, columnData); onMoveColumn?.call(fromIndex, toIndex); - notifyListeners(); + if (notify) notifyListeners(); } void moveColumnItem(String columnId, int fromIndex, int toIndex) { - final columnController = _columnControllers[columnId]; - assert(columnController != null); - if (columnController != null) { - columnController.move(fromIndex, toIndex); + if (getColumnController(columnId)?.move(fromIndex, toIndex) ?? false) { onMoveColumnItem?.call(columnId, fromIndex, toIndex); } } + void addColumnItem(String columnId, ColumnItem item) { + getColumnController(columnId)?.add(item); + } + + void insertColumnItem(String columnId, int index, ColumnItem item) { + getColumnController(columnId)?.insert(index, item); + } + + void removeColumnItem(String columnId, String itemId) { + getColumnController(columnId)?.removeWhere((item) => item.id == itemId); + } + @override @protected void swapColumnItem( From 2b745bc41a4a8cdfe5526d3dece9be44383c9b6a Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 16:03:41 +0800 Subject: [PATCH 044/224] chore: read single select data from board --- .../plugins/board/application/board_bloc.dart | 39 +-- .../board/presentation/board_page.dart | 1 - .../cell/cell_service/cell_service.dart | 2 +- .../cell/select_option_service.dart | 2 +- .../application/field/field_editor_bloc.dart | 5 +- .../grid/application/field/field_service.dart | 154 ------------ .../field/field_type_option_edit_bloc.dart | 14 +- .../field/type_option/date_bloc.dart | 5 +- .../type_option/multi_select_type_option.dart | 12 +- .../field/type_option/number_bloc.dart | 4 +- .../select_option_type_option_bloc.dart | 29 ++- .../single_select_type_option.dart | 10 +- .../type_option_data_controller.dart | 223 ++++++++++++++++++ .../type_option/type_option_service.dart | 85 +------ .../widgets/header/field_cell.dart | 1 + .../widgets/header/field_editor.dart | 2 +- .../header/field_type_option_editor.dart | 3 +- .../widgets/header/grid_header.dart | 1 + .../widgets/header/type_option/builder.dart | 160 ++++++++++--- .../widgets/header/type_option/checkbox.dart | 4 +- .../widgets/header/type_option/rich_text.dart | 4 +- .../widgets/header/type_option/url.dart | 4 +- .../presentation/widgets/row/row_detail.dart | 2 +- .../widgets/toolbar/grid_property.dart | 2 +- 24 files changed, 430 insertions(+), 338 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 1d73e247d0..a2ce5e043f 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; @@ -101,31 +102,33 @@ class BoardBloc extends Bloc { } void _buildColumns(UnmodifiableListView fields) { - List columns = []; - for (final field in fields) { if (field.fieldType == FieldType.SingleSelect) { - // return BoardColumnData(customData: field, id: field.id, desc: "1"); + _buildColumnsFromSingleSelect(field); } } + } - boardDataController.addColumns(columns); + void _buildColumnsFromSingleSelect(GridFieldPB field) { + final typeOptionContext = makeTypeOptionContext( + gridId: _gridDataController.gridId, + field: field, + ); - // final column1 = BoardColumnData(id: "To Do", items: [ - // TextItem("Card 1"), - // TextItem("Card 2"), - // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), - // TextItem("Card 4"), - // ]); - // final column2 = BoardColumnData(id: "In Progress", items: [ - // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - // TextItem("Card 6"), - // ]); + typeOptionContext.loadTypeOptionData( + onCompleted: (singleSelect) { + List columns = singleSelect.options.map((option) { + return BoardColumnData( + id: option.id, + desc: option.name, + customData: option, + ); + }).toList(); - // final column3 = BoardColumnData(id: "Done", items: []); - // boardDataController.addColumn(column1); - // boardDataController.addColumn(column2); - // boardDataController.addColumn(column3); + boardDataController.addColumns(columns); + }, + onError: (err) {}, + ); } Future _loadGrid(Emitter emit) async { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 570c1207df..f961eeaf42 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -3,7 +3,6 @@ import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/board_bloc.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart index 106db1e754..78b6551f0f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart @@ -17,7 +17,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'dart:convert' show utf8; import '../../field/field_cache.dart'; -import '../../field/type_option/type_option_service.dart'; +import '../../field/type_option/type_option_data_controller.dart'; import 'cell_field_notifier.dart'; part 'cell_service.freezed.dart'; part 'cell_data_loader.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart index 9172450ef0..da1fc47170 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart @@ -15,7 +15,7 @@ class SelectOptionService { String get rowId => cellId.rowId; Future> create({required String name}) { - return TypeOptionService(gridId: gridId, fieldId: fieldId) + return TypeOptionFFIService(gridId: gridId, fieldId: fieldId) .newOption(name: name) .then( (result) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart index 57e04cbaf4..05c61eaf55 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart @@ -1,9 +1,10 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; -import 'field_service.dart'; import 'package:dartz/dartz.dart'; +import 'type_option/type_option_data_controller.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + part 'field_editor_bloc.freezed.dart'; class FieldEditorBloc extends Bloc { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index cf4f374a09..39cdccb9ab 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -141,157 +141,3 @@ class GridFieldCellContext with _$GridFieldCellContext { required GridFieldPB field, }) = _GridFieldCellContext; } - -abstract class IFieldTypeOptionLoader { - String get gridId; - Future> load(); - - Future> switchToField( - String fieldId, FieldType fieldType) { - final payload = EditFieldPayloadPB.create() - ..gridId = gridId - ..fieldId = fieldId - ..fieldType = fieldType; - - return GridEventSwitchToField(payload).send(); - } -} - -class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { - @override - final String gridId; - NewFieldTypeOptionLoader({ - required this.gridId, - }); - - @override - Future> load() { - final payload = CreateFieldPayloadPB.create() - ..gridId = gridId - ..fieldType = FieldType.RichText; - - return GridEventCreateFieldTypeOption(payload).send(); - } -} - -class FieldTypeOptionLoader extends IFieldTypeOptionLoader { - @override - final String gridId; - final GridFieldPB field; - - FieldTypeOptionLoader({ - required this.gridId, - required this.field, - }); - - @override - Future> load() { - final payload = GridFieldTypeOptionIdPB.create() - ..gridId = gridId - ..fieldId = field.id - ..fieldType = field.fieldType; - - return GridEventGetFieldTypeOption(payload).send(); - } -} - -class TypeOptionDataController { - final String gridId; - final IFieldTypeOptionLoader _loader; - - late FieldTypeOptionDataPB _data; - final PublishNotifier _fieldNotifier = PublishNotifier(); - - TypeOptionDataController({ - required this.gridId, - required IFieldTypeOptionLoader loader, - }) : _loader = loader; - - Future> loadTypeOptionData() async { - final result = await _loader.load(); - return result.fold( - (data) { - data.freeze(); - _data = data; - _fieldNotifier.value = data.field_2; - return left(unit); - }, - (err) { - Log.error(err); - return right(err); - }, - ); - } - - GridFieldPB get field => _data.field_2; - - set field(GridFieldPB field) { - _updateData(newField: field); - } - - List get typeOptionData => _data.typeOptionData; - - set fieldName(String name) { - _updateData(newName: name); - } - - set typeOptionData(List typeOptionData) { - _updateData(newTypeOptionData: typeOptionData); - } - - void _updateData( - {String? newName, GridFieldPB? newField, List? newTypeOptionData}) { - _data = _data.rebuild((rebuildData) { - if (newName != null) { - rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) { - rebuildField.name = newName; - }); - } - - if (newField != null) { - rebuildData.field_2 = newField; - } - - if (newTypeOptionData != null) { - rebuildData.typeOptionData = newTypeOptionData; - } - }); - - _fieldNotifier.value = _data.field_2; - - FieldService.insertField( - gridId: gridId, - field: field, - typeOptionData: typeOptionData, - ); - } - - Future switchToField(FieldType newFieldType) { - return _loader.switchToField(field.id, newFieldType).then((result) { - return result.fold( - (fieldTypeOptionData) { - _updateData( - newField: fieldTypeOptionData.field_2, - newTypeOptionData: fieldTypeOptionData.typeOptionData, - ); - }, - (err) { - Log.error(err); - }, - ); - }); - } - - void Function() addFieldListener(void Function(GridFieldPB) callback) { - listener() { - callback(field); - } - - _fieldNotifier.addListener(listener); - return listener; - } - - void removeFieldListener(void Function() listener) { - _fieldNotifier.removeListener(listener); - } -} diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart index e098f87d86..ec18c7e5b5 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart @@ -2,12 +2,11 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; - -import 'field_service.dart'; - +import 'type_option/type_option_data_controller.dart'; part 'field_type_option_edit_bloc.freezed.dart'; -class FieldTypeOptionEditBloc extends Bloc { +class FieldTypeOptionEditBloc + extends Bloc { final TypeOptionDataController _dataController; void Function()? _fieldListenFn; @@ -42,7 +41,8 @@ class FieldTypeOptionEditBloc extends Bloc FieldTypeOptionEditState( + factory FieldTypeOptionEditState.initial( + TypeOptionDataController fieldContext) => + FieldTypeOptionEditState( field: fieldContext.field, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart index f995a0bad0..15c052060d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart @@ -1,13 +1,14 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; + +import 'type_option_data_controller.dart'; part 'date_bloc.freezed.dart'; -typedef DateTypeOptionContext = TypeOptionWidgetContext; +typedef DateTypeOptionContext = TypeOptionContext; class DateTypeOptionDataParser extends TypeOptionDataParser { @override diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart index 2f7d2dbc5a..a05e5ea2ff 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart @@ -1,21 +1,21 @@ -import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'dart:async'; -import 'package:protobuf/protobuf.dart'; import 'select_option_type_option_bloc.dart'; +import 'type_option_data_controller.dart'; import 'type_option_service.dart'; +import 'package:protobuf/protobuf.dart'; class MultiSelectTypeOptionContext - extends TypeOptionWidgetContext + extends TypeOptionContext with SelectOptionTypeOptionAction { - final TypeOptionService service; + final TypeOptionFFIService service; MultiSelectTypeOptionContext({ required MultiSelectTypeOptionWidgetDataParser dataParser, required TypeOptionDataController dataController, - }) : service = TypeOptionService( + }) : service = TypeOptionFFIService( gridId: dataController.gridId, fieldId: dataController.field.id, ), @@ -59,7 +59,7 @@ class MultiSelectTypeOptionContext } @override - List Function(SelectOptionPB) get udpateOption { + List Function(SelectOptionPB) get updateOption { return (SelectOptionPB option) { typeOption.freeze(); typeOption = typeOption.rebuild((typeOption) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart index 7228adf90e..fc3d8b0822 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart @@ -1,14 +1,14 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; +import 'type_option_data_controller.dart'; part 'number_bloc.freezed.dart'; -typedef NumberTypeOptionContext = TypeOptionWidgetContext; +typedef NumberTypeOptionContext = TypeOptionContext; class NumberTypeOptionWidgetDataParser extends TypeOptionDataParser { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart index b77b9b86cd..6e4aca6cc9 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart @@ -10,10 +10,11 @@ abstract class SelectOptionTypeOptionAction { List Function(SelectOptionPB) get deleteOption; - List Function(SelectOptionPB) get udpateOption; + List Function(SelectOptionPB) get updateOption; } -class SelectOptionTypeOptionBloc extends Bloc { +class SelectOptionTypeOptionBloc + extends Bloc { final SelectOptionTypeOptionAction typeOptionAction; SelectOptionTypeOptionBloc({ @@ -24,7 +25,8 @@ class SelectOptionTypeOptionBloc extends Bloc options = await typeOptionAction.insertOption(optionName); + final List options = + await typeOptionAction.insertOption(optionName); emit(state.copyWith(options: options)); }, addingOption: () { @@ -34,11 +36,13 @@ class SelectOptionTypeOptionBloc extends Bloc options = typeOptionAction.udpateOption(option); + final List options = + typeOptionAction.updateOption(option); emit(state.copyWith(options: options)); }, deleteOption: (option) { - final List options = typeOptionAction.deleteOption(option); + final List options = + typeOptionAction.deleteOption(option); emit(state.copyWith(options: options)); }, ); @@ -54,11 +58,15 @@ class SelectOptionTypeOptionBloc extends Bloc newOptionName, }) = _SelectOptionTyepOptionState; - factory SelectOptionTypeOptionState.initial(List options) => SelectOptionTypeOptionState( + factory SelectOptionTypeOptionState.initial(List options) => + SelectOptionTypeOptionState( options: options, isEditingOption: false, newOptionName: none(), diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart index bc995a4237..e4de10a3ef 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart @@ -1,21 +1,21 @@ -import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; import 'select_option_type_option_bloc.dart'; +import 'type_option_data_controller.dart'; import 'type_option_service.dart'; class SingleSelectTypeOptionContext - extends TypeOptionWidgetContext + extends TypeOptionContext with SelectOptionTypeOptionAction { - final TypeOptionService service; + final TypeOptionFFIService service; SingleSelectTypeOptionContext({ required SingleSelectTypeOptionWidgetDataParser dataBuilder, required TypeOptionDataController dataController, - }) : service = TypeOptionService( + }) : service = TypeOptionFFIService( gridId: dataController.gridId, fieldId: dataController.field.id, ), @@ -59,7 +59,7 @@ class SingleSelectTypeOptionContext } @override - List Function(SelectOptionPB) get udpateOption { + List Function(SelectOptionPB) get updateOption { return (SelectOptionPB option) { typeOption.freeze(); typeOption = typeOption.rebuild((typeOption) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart new file mode 100644 index 0000000000..75b373f787 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart @@ -0,0 +1,223 @@ +import 'package:flowy_infra/notifier.dart'; +import 'package:flowy_sdk/dispatch/dispatch.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; +import 'package:dartz/dartz.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:flowy_sdk/log.dart'; + +abstract class TypeOptionDataParser { + T fromBuffer(List buffer); +} + +class TypeOptionContext { + T? _typeOptionObject; + final TypeOptionDataParser dataParser; + final TypeOptionDataController _dataController; + + TypeOptionContext({ + required this.dataParser, + required TypeOptionDataController dataController, + }) : _dataController = dataController; + + String get gridId => _dataController.gridId; + + Future loadTypeOptionData({ + required void Function(T) onCompleted, + required void Function(FlowyError) onError, + }) async { + await _dataController.loadTypeOptionData().then((result) { + result.fold((l) => null, (err) => onError(err)); + }); + + onCompleted(typeOption); + } + + T get typeOption { + if (_typeOptionObject != null) { + return _typeOptionObject!; + } + + final T object = _dataController.getTypeOption(dataParser); + _typeOptionObject = object; + return object; + } + + set typeOption(T typeOption) { + _dataController.typeOptionData = typeOption.writeToBuffer(); + _typeOptionObject = typeOption; + } +} + +abstract class TypeOptionFieldDelegate { + void onFieldChanged(void Function(String) callback); + void dispose(); +} + +abstract class IFieldTypeOptionLoader { + String get gridId; + Future> load(); + + Future> switchToField( + String fieldId, FieldType fieldType) { + final payload = EditFieldPayloadPB.create() + ..gridId = gridId + ..fieldId = fieldId + ..fieldType = fieldType; + + return GridEventSwitchToField(payload).send(); + } +} + +class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { + @override + final String gridId; + NewFieldTypeOptionLoader({ + required this.gridId, + }); + + @override + Future> load() { + final payload = CreateFieldPayloadPB.create() + ..gridId = gridId + ..fieldType = FieldType.RichText; + + return GridEventCreateFieldTypeOption(payload).send(); + } +} + +class FieldTypeOptionLoader extends IFieldTypeOptionLoader { + @override + final String gridId; + final GridFieldPB field; + + FieldTypeOptionLoader({ + required this.gridId, + required this.field, + }); + + @override + Future> load() { + final payload = GridFieldTypeOptionIdPB.create() + ..gridId = gridId + ..fieldId = field.id + ..fieldType = field.fieldType; + + return GridEventGetFieldTypeOption(payload).send(); + } +} + +class TypeOptionDataController { + final String gridId; + final IFieldTypeOptionLoader loader; + late FieldTypeOptionDataPB _data; + final PublishNotifier _fieldNotifier = PublishNotifier(); + + TypeOptionDataController({ + required this.gridId, + required this.loader, + GridFieldPB? field, + }) { + if (field != null) { + _data = FieldTypeOptionDataPB.create() + ..gridId = gridId + ..field_2 = field; + } + } + + Future> loadTypeOptionData() async { + final result = await loader.load(); + return result.fold( + (data) { + data.freeze(); + _data = data; + _fieldNotifier.value = data.field_2; + return left(unit); + }, + (err) { + Log.error(err); + return right(err); + }, + ); + } + + GridFieldPB get field { + return _data.field_2; + } + + set field(GridFieldPB field) { + _updateData(newField: field); + } + + T getTypeOption(TypeOptionDataParser parser) { + return parser.fromBuffer(_data.typeOptionData); + } + + set fieldName(String name) { + _updateData(newName: name); + } + + set typeOptionData(List typeOptionData) { + _updateData(newTypeOptionData: typeOptionData); + } + + void _updateData({ + String? newName, + GridFieldPB? newField, + List? newTypeOptionData, + }) { + _data = _data.rebuild((rebuildData) { + if (newName != null) { + rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) { + rebuildField.name = newName; + }); + } + + if (newField != null) { + rebuildData.field_2 = newField; + } + + if (newTypeOptionData != null) { + rebuildData.typeOptionData = newTypeOptionData; + } + }); + + _fieldNotifier.value = _data.field_2; + + FieldService.insertField( + gridId: gridId, + field: field, + typeOptionData: _data.typeOptionData, + ); + } + + Future switchToField(FieldType newFieldType) { + return loader.switchToField(field.id, newFieldType).then((result) { + return result.fold( + (fieldTypeOptionData) { + _updateData( + newField: fieldTypeOptionData.field_2, + newTypeOptionData: fieldTypeOptionData.typeOptionData, + ); + }, + (err) { + Log.error(err); + }, + ); + }); + } + + void Function() addFieldListener(void Function(GridFieldPB) callback) { + listener() { + callback(field); + } + + _fieldNotifier.addListener(listener); + return listener; + } + + void removeFieldListener(void Function() listener) { + _fieldNotifier.removeListener(listener); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_service.dart index d7873e0c86..5407515f62 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_service.dart @@ -1,19 +1,14 @@ -import 'dart:typed_data'; - -import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; -import 'package:protobuf/protobuf.dart'; -class TypeOptionService { +class TypeOptionFFIService { final String gridId; final String fieldId; - TypeOptionService({ + TypeOptionFFIService({ required this.gridId, required this.fieldId, }); @@ -29,79 +24,3 @@ class TypeOptionService { return GridEventNewSelectOption(payload).send(); } } - -abstract class TypeOptionDataParser { - T fromBuffer(List buffer); -} - -class TypeOptionWidgetContext { - T? _typeOptionObject; - final TypeOptionDataController _dataController; - final TypeOptionDataParser dataParser; - - TypeOptionWidgetContext({ - required this.dataParser, - required TypeOptionDataController dataController, - }) : _dataController = dataController; - - String get gridId => _dataController.gridId; - - GridFieldPB get field => _dataController.field; - - T get typeOption { - if (_typeOptionObject != null) { - return _typeOptionObject!; - } - - final T object = dataParser.fromBuffer(_dataController.typeOptionData); - _typeOptionObject = object; - return object; - } - - set typeOption(T typeOption) { - _dataController.typeOptionData = typeOption.writeToBuffer(); - _typeOptionObject = typeOption; - } -} - -abstract class TypeOptionFieldDelegate { - void onFieldChanged(void Function(String) callback); - void dispose(); -} - -class TypeOptionContext2 { - final String gridId; - final GridFieldPB field; - final FieldService _fieldService; - T? _data; - final TypeOptionDataParser dataBuilder; - - TypeOptionContext2({ - required this.gridId, - required this.field, - required this.dataBuilder, - Uint8List? data, - }) : _fieldService = FieldService(gridId: gridId, fieldId: field.id) { - if (data != null) { - _data = dataBuilder.fromBuffer(data); - } - } - - Future> typeOptionData() { - if (_data != null) { - return Future(() => left(_data!)); - } - - return _fieldService - .getFieldTypeOptionData(fieldType: field.fieldType) - .then((result) { - return result.fold( - (data) { - _data = dataBuilder.fromBuffer(data.typeOptionData); - return left(_data!); - }, - (err) => right(err), - ); - }); - } -} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart index aec88f5eaf..f66d5bf6d8 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/plugins/grid/application/field/field_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart index 30a76a34bb..1a9a0ead83 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart @@ -1,5 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart'; -import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart index c2a12bb7ad..50a218fd0d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:dartz/dartz.dart' show Either; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; @@ -94,8 +95,8 @@ class _FieldTypeOptionEditorState extends State { return makeTypeOptionWidget( context: context, - dataController: widget.dataController, overlayDelegate: overlayDelegate, + dataController: widget.dataController, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart index caa40b2e7a..cad7e300f8 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 787f2091a9..d89dc26a68 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -1,6 +1,15 @@ import 'dart:typed_data'; import 'package:app_flowy/plugins/grid/application/field/type_option/multi_select_type_option.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:protobuf/protobuf.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; @@ -39,70 +48,147 @@ Widget? makeTypeOptionWidget({ required TypeOptionDataController dataController, required TypeOptionOverlayDelegate overlayDelegate, }) { - final builder = makeTypeOptionWidgetBuilder(dataController, overlayDelegate); + final builder = makeTypeOptionWidgetBuilder( + dataController: dataController, + overlayDelegate: overlayDelegate, + ); return builder.build(context); } -TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder( - TypeOptionDataController dataController, - TypeOptionOverlayDelegate overlayDelegate, -) { +TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ + required TypeOptionDataController dataController, + required TypeOptionOverlayDelegate overlayDelegate, +}) { + final gridId = dataController.gridId; + final fieldType = dataController.field.fieldType; + switch (dataController.field.fieldType) { case FieldType.Checkbox: - final context = CheckboxTypeOptionContext( - dataController: dataController, - dataParser: CheckboxTypeOptionWidgetDataParser(), + return CheckboxTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ), ); - return CheckboxTypeOptionWidgetBuilder(context); case FieldType.DateTime: - final context = DateTypeOptionContext( - dataController: dataController, - dataParser: DateTypeOptionDataParser(), - ); return DateTypeOptionWidgetBuilder( - context, + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ), overlayDelegate, ); case FieldType.SingleSelect: - final context = SingleSelectTypeOptionContext( - dataController: dataController, - dataBuilder: SingleSelectTypeOptionWidgetDataParser(), - ); return SingleSelectTypeOptionWidgetBuilder( - context, + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ) as SingleSelectTypeOptionContext, overlayDelegate, ); case FieldType.MultiSelect: - final context = MultiSelectTypeOptionContext( - dataController: dataController, - dataParser: MultiSelectTypeOptionWidgetDataParser(), - ); return MultiSelectTypeOptionWidgetBuilder( - context, + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ) as MultiSelectTypeOptionContext, overlayDelegate, ); case FieldType.Number: - final context = NumberTypeOptionContext( - dataController: dataController, - dataParser: NumberTypeOptionWidgetDataParser(), - ); return NumberTypeOptionWidgetBuilder( - context, + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ), overlayDelegate, ); case FieldType.RichText: - final context = RichTextTypeOptionContext( - dataController: dataController, - dataParser: RichTextTypeOptionWidgetDataParser(), + return RichTextTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ), ); - return RichTextTypeOptionWidgetBuilder(context); case FieldType.URL: - final context = URLTypeOptionContext( - dataController: dataController, - dataParser: URLTypeOptionWidgetDataParser(), + return URLTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: fieldType, + dataController: dataController, + ), ); - return URLTypeOptionWidgetBuilder(context); } throw UnimplementedError; } + +TypeOptionContext makeTypeOptionContext({ + required String gridId, + required GridFieldPB field, +}) { + final loader = FieldTypeOptionLoader(gridId: gridId, field: field); + final dataController = TypeOptionDataController( + gridId: gridId, + loader: loader, + field: field, + ); + return makeTypeOptionContextWithDataController( + gridId: gridId, + fieldType: field.fieldType, + dataController: dataController, + ); +} + +TypeOptionContext + makeTypeOptionContextWithDataController({ + required String gridId, + required FieldType fieldType, + required TypeOptionDataController dataController, +}) { + switch (fieldType) { + case FieldType.Checkbox: + return CheckboxTypeOptionContext( + dataController: dataController, + dataParser: CheckboxTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.DateTime: + return DateTypeOptionContext( + dataController: dataController, + dataParser: DateTypeOptionDataParser(), + ) as TypeOptionContext; + case FieldType.SingleSelect: + return SingleSelectTypeOptionContext( + dataController: dataController, + dataBuilder: SingleSelectTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.MultiSelect: + return MultiSelectTypeOptionContext( + dataController: dataController, + dataParser: MultiSelectTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.Number: + return NumberTypeOptionContext( + dataController: dataController, + dataParser: NumberTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + case FieldType.RichText: + return RichTextTypeOptionContext( + dataController: dataController, + dataParser: RichTextTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + + case FieldType.URL: + return URLTypeOptionContext( + dataController: dataController, + dataParser: URLTypeOptionWidgetDataParser(), + ) as TypeOptionContext; + } + + throw UnimplementedError; +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart index fdc5719181..fc4c4c16a7 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart @@ -1,9 +1,9 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -typedef CheckboxTypeOptionContext = TypeOptionWidgetContext; +typedef CheckboxTypeOptionContext = TypeOptionContext; class CheckboxTypeOptionWidgetDataParser extends TypeOptionDataParser { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart index a4291f91ce..4ee9e1e6ae 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart @@ -1,9 +1,9 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -typedef RichTextTypeOptionContext = TypeOptionWidgetContext; +typedef RichTextTypeOptionContext = TypeOptionContext; class RichTextTypeOptionWidgetDataParser extends TypeOptionDataParser { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart index 894a1b1e5c..3521b336cf 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart @@ -1,9 +1,9 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -typedef URLTypeOptionContext = TypeOptionWidgetContext; +typedef URLTypeOptionContext = TypeOptionContext; class URLTypeOptionWidgetDataParser extends TypeOptionDataParser { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index bfaef332a9..3412d54e1e 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -1,5 +1,5 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; -import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index 28f2b860ff..290da43f95 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart @@ -1,5 +1,5 @@ +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:app_flowy/plugins/grid/application/setting/property_bloc.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:flowy_infra/image.dart'; From a26b6938079c5d77bed6c72493bbdeed178f4742 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 15:59:03 +0800 Subject: [PATCH 045/224] feat: add comments to HTMLConvertert --- .../lib/src/infra/html_converter.dart | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 8444bb7394..eb4eb0f653 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -31,6 +31,12 @@ extension on Color { /// Converting the HTML to nodes class HTMLToNodesConverter { final html.Document _document; + + /// This flag is used for parsing HTML pasting from Google Docs + /// Google docs wraps the the content inside the `` tag. It's strange. + /// + /// If a `` element is parsing in the

, we regard it as as text spans. + /// Otherwise, it's parsed as a container. bool _inParagraph = false; HTMLToNodesConverter(String htmlString) : _document = parse(htmlString); @@ -51,7 +57,7 @@ class HTMLToNodesConverter { child.localName == tagStrong) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { - // Google docs wraps the the content inside the tag. + // Google docs wraps the the content inside the `` tag. // It's strange if (!_inParagraph) { result.addAll(_handleBTag(child)); @@ -152,6 +158,8 @@ class HTMLToNodesConverter { return attrs.isEmpty ? null : attrs; } + /// Try to parse the `rgba(red, greed, blue, alpha)` + /// from the string. Color? _tryParseCssColorString(String? colorString) { if (colorString == null) { return null; @@ -200,6 +208,10 @@ class HTMLToNodesConverter { } } + /// A container contains a will + /// be regarded as a checkbox block. + /// + /// A container contains a will be regarded as a image block Node _handleRichText(html.Element element, [Map? attributes]) { final image = element.querySelector(tagImage); @@ -288,11 +300,23 @@ class HTMLToNodesConverter { } } +/// [NodesToHTMLConverter] is used to convert the nodes to HTML. +/// Can be used to copy & paste, exporting the document. class NodesToHTMLConverter { final List nodes; final int? startOffset; final int? endOffset; final List _result = []; + + /// According to the W3C specs. The bullet list should be wrapped as + /// + ///

    + ///
  • xxx
  • + ///
  • xxx
  • + ///
  • xxx
  • + ///
+ /// + /// This container is used to save the list elements temporarily. html.Element? _stashListContainer; NodesToHTMLConverter( From 32c9433cf0be18178474c070119f95f3a342e079 Mon Sep 17 00:00:00 2001 From: Ian Su Date: Wed, 10 Aug 2022 16:32:32 +0800 Subject: [PATCH 046/224] feat: update icon.svg files --- .../app_flowy/assets/images/emoji/1F42F.svg | 32 +++++++++++++++ .../app_flowy/assets/images/emoji/1F431.svg | 24 ++++++++++++ .../app_flowy/assets/images/emoji/1F435.svg | 28 +++++++++++++ .../app_flowy/assets/images/emoji/1F43A.svg | 28 +++++++++++++ .../app_flowy/assets/images/emoji/1F600.svg | 17 ++++++++ .../app_flowy/assets/images/emoji/1F984.svg | 21 ++++++++++ .../app_flowy/assets/images/emoji/1F9CC.svg | 33 ++++++++++++++++ .../app_flowy/assets/images/emoji/1F9DB.svg | 26 +++++++++++++ .../images/emoji/1F9DD-200D-2642-FE0F.svg | 39 +++++++++++++++++++ .../images/emoji/1F9DE-200D-2642-FE0F.svg | 28 +++++++++++++ .../app_flowy/assets/images/emoji/1F9DF.svg | 28 +++++++++++++ .../app_flowy/assets/images/emoji/close.svg | 4 -- .../assets/images/emoji/favorite_active.svg | 5 --- .../images/emoji/favorite_inacvtive.svg | 5 --- .../app_flowy/assets/images/emoji/image.svg | 6 --- .../app_flowy/assets/images/emoji/page.svg | 6 --- 16 files changed, 304 insertions(+), 26 deletions(-) create mode 100644 frontend/app_flowy/assets/images/emoji/1F42F.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F431.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F435.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F43A.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F600.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F984.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F9CC.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F9DB.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg create mode 100644 frontend/app_flowy/assets/images/emoji/1F9DF.svg delete mode 100644 frontend/app_flowy/assets/images/emoji/close.svg delete mode 100644 frontend/app_flowy/assets/images/emoji/favorite_active.svg delete mode 100644 frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg delete mode 100644 frontend/app_flowy/assets/images/emoji/image.svg delete mode 100644 frontend/app_flowy/assets/images/emoji/page.svg diff --git a/frontend/app_flowy/assets/images/emoji/1F42F.svg b/frontend/app_flowy/assets/images/emoji/1F42F.svg new file mode 100644 index 0000000000..a6e8e3e81f --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F42F.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F431.svg b/frontend/app_flowy/assets/images/emoji/1F431.svg new file mode 100644 index 0000000000..26aa279abc --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F431.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F435.svg b/frontend/app_flowy/assets/images/emoji/1F435.svg new file mode 100644 index 0000000000..0220a6e58e --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F435.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F43A.svg b/frontend/app_flowy/assets/images/emoji/1F43A.svg new file mode 100644 index 0000000000..3e29b3a6a9 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F43A.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F600.svg b/frontend/app_flowy/assets/images/emoji/1F600.svg new file mode 100644 index 0000000000..e9e1d0ea88 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F600.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F984.svg b/frontend/app_flowy/assets/images/emoji/1F984.svg new file mode 100644 index 0000000000..a5f8206cbc --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F984.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9CC.svg b/frontend/app_flowy/assets/images/emoji/1F9CC.svg new file mode 100644 index 0000000000..eb30038228 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9CC.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DB.svg b/frontend/app_flowy/assets/images/emoji/1F9DB.svg new file mode 100644 index 0000000000..590829d25c --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DB.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg b/frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg new file mode 100644 index 0000000000..62e5101f53 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DD-200D-2642-FE0F.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg b/frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg new file mode 100644 index 0000000000..e662de70a6 --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DE-200D-2642-FE0F.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/1F9DF.svg b/frontend/app_flowy/assets/images/emoji/1F9DF.svg new file mode 100644 index 0000000000..e2ea11f33a --- /dev/null +++ b/frontend/app_flowy/assets/images/emoji/1F9DF.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app_flowy/assets/images/emoji/close.svg b/frontend/app_flowy/assets/images/emoji/close.svg deleted file mode 100644 index 822d63d82d..0000000000 --- a/frontend/app_flowy/assets/images/emoji/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/favorite_active.svg b/frontend/app_flowy/assets/images/emoji/favorite_active.svg deleted file mode 100644 index 859822c00e..0000000000 --- a/frontend/app_flowy/assets/images/emoji/favorite_active.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg b/frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg deleted file mode 100644 index f56e138a31..0000000000 --- a/frontend/app_flowy/assets/images/emoji/favorite_inacvtive.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/image.svg b/frontend/app_flowy/assets/images/emoji/image.svg deleted file mode 100644 index bf4fdffa85..0000000000 --- a/frontend/app_flowy/assets/images/emoji/image.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/app_flowy/assets/images/emoji/page.svg b/frontend/app_flowy/assets/images/emoji/page.svg deleted file mode 100644 index 1b7405cbc1..0000000000 --- a/frontend/app_flowy/assets/images/emoji/page.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file From e095a176832f644eb22b387887ac7b386e9c0dfd Mon Sep 17 00:00:00 2001 From: Aryman Date: Wed, 10 Aug 2022 14:46:39 +0530 Subject: [PATCH 047/224] fix: bug where context menu options wouldn't work unless page was selcted --- .../home/menu/app/section/disclosure_action.dart | 12 +++++++++++- .../presentation/home/menu/app/section/item.dart | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart index 7375f6a113..019f674b01 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/disclosure_action.dart @@ -64,13 +64,17 @@ class ViewDisclosureButton extends StatelessWidget class ViewDisclosureRegion extends StatelessWidget with ActionList, FlowyOverlayDelegate { final Widget child; + final Function() onTap; final Function(dartz.Option) onSelected; final _items = ViewDisclosureAction.values .map((action) => ViewDisclosureActionWrapper(action)) .toList(); ViewDisclosureRegion( - {Key? key, required this.onSelected, required this.child}) + {Key? key, + required this.onSelected, + required this.onTap, + required this.child}) : super(key: key); @override @@ -96,6 +100,11 @@ class ViewDisclosureRegion extends StatelessWidget ); }; + @override + void didRemove() { + onSelected(dartz.none()); + } + void _handleClick(PointerDownEvent event, BuildContext context) { if (event.kind == PointerDeviceKind.mouse && event.buttons == kSecondaryMouseButton) { @@ -103,6 +112,7 @@ class ViewDisclosureRegion extends StatelessWidget Offset position = box.localToGlobal(Offset.zero); double x = event.position.dx - position.dx - box.size.width; double y = event.position.dy - position.dy - box.size.height; + onTap(); show(context, anchorOffset: Offset(x, y)); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index 3ca8577d8c..bfc51f4bf9 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -86,6 +86,8 @@ class ViewSectionItem extends StatelessWidget { } return ViewDisclosureRegion( + onTap: () => + context.read().add(const ViewEvent.setIsEditing(true)), onSelected: (action) { context.read().add(const ViewEvent.setIsEditing(false)); _handleAction(context, action); From 737e374a5491c9f83cdd1de278e42224afda8c5d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 16:44:37 +0800 Subject: [PATCH 048/224] refactor: text-delta --- .../flowy_editor/lib/src/document/node.dart | 4 +- .../lib/src/document/text_delta.dart | 61 +++++++++++-------- .../src/extensions/text_node_extensions.dart | 2 +- .../lib/src/infra/html_converter.dart | 6 +- .../src/operation/transaction_builder.dart | 11 ++-- .../src/render/rich_text/flowy_rich_text.dart | 2 +- .../copy_paste_handler.dart | 5 +- .../flowy_editor/test/delta_test.dart | 2 +- 8 files changed, 47 insertions(+), 46 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 0b6b941aaa..8ed5d10297 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -219,7 +219,5 @@ class TextNode extends Node { delta: delta ?? this.delta, ); - // TODO: It's unneccesry to compute everytime. - String toRawString() => - _delta.operations.whereType().map((op) => op.content).join(); + String toRawString() => _delta.toRawString(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 72378f80c6..5c81a039ed 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -257,8 +257,8 @@ TextOperation? _textOperationFromJson(Map json) { } // basically copy from: https://github.com/quilljs/delta -class Delta { - final List operations; +class Delta extends Iterable { + final List _operations; factory Delta.fromJson(List list) { final operations = []; @@ -273,9 +273,9 @@ class Delta { return Delta(operations); } - Delta([List? ops]) : operations = ops ?? []; + Delta([List? ops]) : _operations = ops ?? []; - Delta addAll(List textOps) { + Delta addAll(Iterable textOps) { textOps.forEach(add); return this; } @@ -285,8 +285,8 @@ class Delta { return this; } - if (operations.isNotEmpty) { - final lastOp = operations.last; + if (_operations.isNotEmpty) { + final lastOp = _operations.last; if (lastOp is TextDelete && textOp is TextDelete) { lastOp.length += textOp.length; return this; @@ -299,9 +299,9 @@ class Delta { // if there is an delete before the insert // swap the order if (lastOp is TextDelete && textOp is TextInsert) { - operations.removeLast(); - operations.add(textOp); - operations.add(lastOp); + _operations.removeLast(); + _operations.add(textOp); + _operations.add(lastOp); return this; } if (lastOp is TextRetain && textOp is TextRetain) { @@ -311,13 +311,13 @@ class Delta { } } - operations.add(textOp); + _operations.add(textOp); return this; } Delta slice(int start, [int? end]) { final result = Delta(); - final iterator = _OpIterator(operations); + final iterator = _OpIterator(_operations); int index = 0; while ((end == null || index < end) && iterator.hasNext) { @@ -351,13 +351,13 @@ class Delta { } int get length { - return operations.fold( + return _operations.fold( 0, (previousValue, element) => previousValue + element.length); } Delta compose(Delta other) { - final thisIter = _OpIterator(operations); - final otherIter = _OpIterator(other.operations); + final thisIter = _OpIterator(_operations); + final otherIter = _OpIterator(other._operations); final ops = []; final firstOther = otherIter.peek(); @@ -405,7 +405,7 @@ class Delta { // Optimization if rest of other is just retain if (!otherIter.hasNext && - delta.operations[delta.operations.length - 1] == newOp) { + delta._operations[delta._operations.length - 1] == newOp) { final rest = Delta(thisIter.rest()); return delta.concat(rest).chop(); } @@ -419,21 +419,21 @@ class Delta { } Delta concat(Delta other) { - var ops = [...operations]; - if (other.operations.isNotEmpty) { - ops.add(other.operations[0]); - ops.addAll(other.operations.sublist(1)); + var ops = [..._operations]; + if (other._operations.isNotEmpty) { + ops.add(other._operations[0]); + ops.addAll(other._operations.sublist(1)); } return Delta(ops); } Delta chop() { - if (operations.isEmpty) { + if (_operations.isEmpty) { return this; } - final lastOp = operations.last; + final lastOp = _operations.last; if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { - operations.removeLast(); + _operations.removeLast(); } return this; } @@ -443,17 +443,17 @@ class Delta { if (other is! Delta) { return false; } - return listEquals(operations, other.operations); + return listEquals(_operations, other._operations); } @override int get hashCode { - return hashList(operations); + return hashList(_operations); } Delta invert(Delta base) { final inverted = Delta(); - operations.fold(0, (int previousValue, op) { + _operations.fold(0, (int previousValue, op) { if (op is TextInsert) { inverted.delete(op.length); } else if (op is TextRetain && op.attributes == null) { @@ -462,7 +462,7 @@ class Delta { } else if (op is TextDelete || op is TextRetain) { final length = op.length; final slice = base.slice(previousValue, previousValue + length); - for (final baseOp in slice.operations) { + for (final baseOp in slice._operations) { if (op is TextDelete) { inverted.add(baseOp); } else if (op is TextRetain && op.attributes != null) { @@ -478,6 +478,13 @@ class Delta { } List toJson() { - return operations.map((e) => e.toJson()).toList(); + return _operations.map((e) => e.toJson()).toList(); } + + // TODO: It's unneccesry to compute everytime. + String toRawString() => + _operations.whereType().map((op) => op.content).join(); + + @override + Iterator get iterator => _operations.iterator; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart index 7b068f18d7..131255ab63 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart @@ -19,7 +19,7 @@ extension TextNodeExtension on TextNode { allSatisfyInSelection(StyleKey.strikethrough, selection); bool allSatisfyInSelection(String styleKey, Selection selection) { - final ops = delta.operations.whereType(); + final ops = delta.whereType(); var start = 0; for (final op in ops) { if (start >= selection.end.offset) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index eb4eb0f653..e16237c0fa 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -71,7 +71,7 @@ class HTMLToNodesConverter { delta.insert(child.text ?? ""); } } - if (delta.operations.isNotEmpty) { + if (delta.isNotEmpty) { result.add(TextNode(type: "text", delta: delta)); } return result; @@ -101,7 +101,7 @@ class HTMLToNodesConverter { } else { final delta = Delta(); delta.insert(element.text); - if (delta.operations.isNotEmpty) { + if (delta.isNotEmpty) { return [TextNode(type: "text", delta: delta)]; } } @@ -446,7 +446,7 @@ class NodesToHTMLConverter { childNodes.add(node); } - for (final op in delta.operations) { + for (final op in delta) { if (op is TextInsert) { final attributes = op.attributes; if (attributes != null) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index 4c080f06e2..918f68118d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -94,7 +94,7 @@ class TransactionBuilder { () => Delta() ..retain(firstOffset ?? firstLength) ..delete(firstLength - (firstOffset ?? firstLength)) - ..addAll(secondNode.delta.slice(secondOffset, secondLength).operations), + ..addAll(secondNode.delta.slice(secondOffset, secondLength)), ); afterSelection = Selection.collapsed( Position( @@ -108,11 +108,8 @@ class TransactionBuilder { [Attributes? attributes]) { var newAttributes = attributes; if (index != 0 && attributes == null) { - newAttributes = node.delta - .slice(max(index - 1, 0), index) - .operations - .first - .attributes; + newAttributes = + node.delta.slice(max(index - 1, 0), index).first.attributes; } textEdit( node, @@ -140,7 +137,7 @@ class TransactionBuilder { [Attributes? attributes]) { var newAttributes = attributes; if (attributes == null) { - final ops = node.delta.slice(index, index + length).operations; + final ops = node.delta.slice(index, index + length); if (ops.isNotEmpty) { newAttributes = ops.first.attributes; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 350a5c71e6..15c40a1121 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -198,7 +198,7 @@ class _FlowyRichTextState extends State with Selectable { } TextSpan get _textSpan => TextSpan( - children: widget.textNode.delta.operations + children: widget.textNode.delta .whereType() .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 948e5233fc..debce245c2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -175,8 +175,7 @@ _handlePastePlainText(EditorState editorState, String plainText) { final nodes = remains.map((e) { if (index++ == remains.length - 1) { return TextNode( - type: "text", - delta: Delta().insert(e).addAll(insertedLineSuffix.operations)); + type: "text", delta: Delta().insert(e).addAll(insertedLineSuffix)); } return TextNode(type: "text", delta: Delta().insert(e)); }).toList(); @@ -246,7 +245,7 @@ _deleteSelectedContent(EditorState editorState) { if (endNode is TextNode) { final remain = endNode.delta.slice(selection.end.offset); - delta.addAll(remain.operations); + delta.addAll(remain); } return delta; diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 89001e79cf..c62f600266 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -19,7 +19,7 @@ void main() { }).delete(4); final restores = delta.compose(death); - expect(restores.operations, [ + expect(restores.toList(), [ TextInsert('Gandalf', {'bold': true}), TextInsert(' the '), TextInsert('White', {'color': '#fff'}), From eb519f4e11884ffcf1e2a28b56104d73b4a64698 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 16:48:19 +0800 Subject: [PATCH 049/224] feat: add cache to text_delta --- .../flowy_editor/lib/src/document/text_delta.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 5c81a039ed..c886f9d5c8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -259,6 +259,7 @@ TextOperation? _textOperationFromJson(Map json) { // basically copy from: https://github.com/quilljs/delta class Delta extends Iterable { final List _operations; + String? _rawString; factory Delta.fromJson(List list) { final operations = []; @@ -284,6 +285,7 @@ class Delta extends Iterable { if (textOp.isEmpty) { return this; } + _rawString = null; if (_operations.isNotEmpty) { final lastOp = _operations.last; @@ -431,6 +433,7 @@ class Delta extends Iterable { if (_operations.isEmpty) { return this; } + _rawString = null; final lastOp = _operations.last; if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { _operations.removeLast(); @@ -481,9 +484,11 @@ class Delta extends Iterable { return _operations.map((e) => e.toJson()).toList(); } - // TODO: It's unneccesry to compute everytime. - String toRawString() => - _operations.whereType().map((op) => op.content).join(); + String toRawString() { + _rawString ??= + _operations.whereType().map((op) => op.content).join(); + return _rawString!; + } @override Iterator get iterator => _operations.iterator; From 45556ae015a703b16afb0e9ae536611cd9511b85 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 17:00:04 +0800 Subject: [PATCH 050/224] refactor: flutter style optional chaining --- .../lib/src/document/text_delta.dart | 46 ++-- .../src/operation/transaction_builder.dart | 27 +- .../copy_paste_handler.dart | 42 +-- .../flowy_editor/test/delta_test.dart | 255 +++++++++++------- 4 files changed, 217 insertions(+), 153 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index c886f9d5c8..6626ba7a86 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -276,14 +276,13 @@ class Delta extends Iterable { Delta([List? ops]) : _operations = ops ?? []; - Delta addAll(Iterable textOps) { + void addAll(Iterable textOps) { textOps.forEach(add); - return this; } - Delta add(TextOperation textOp) { + void add(TextOperation textOp) { if (textOp.isEmpty) { - return this; + return; } _rawString = null; @@ -291,12 +290,12 @@ class Delta extends Iterable { final lastOp = _operations.last; if (lastOp is TextDelete && textOp is TextDelete) { lastOp.length += textOp.length; - return this; + return; } if (mapEquals(lastOp.attributes, textOp.attributes)) { if (lastOp is TextInsert && textOp is TextInsert) { lastOp.content += textOp.content; - return this; + return; } // if there is an delete before the insert // swap the order @@ -304,17 +303,16 @@ class Delta extends Iterable { _operations.removeLast(); _operations.add(textOp); _operations.add(lastOp); - return this; + return; } if (lastOp is TextRetain && textOp is TextRetain) { lastOp.length += textOp.length; - return this; + return; } } } _operations.add(textOp); - return this; } Delta slice(int start, [int? end]) { @@ -337,20 +335,13 @@ class Delta extends Iterable { return result; } - Delta insert(String content, [Attributes? attributes]) { - final op = TextInsert(content, attributes); - return add(op); - } + void insert(String content, [Attributes? attributes]) => + add(TextInsert(content, attributes)); - Delta retain(int length, [Attributes? attributes]) { - final op = TextRetain(length, attributes); - return add(op); - } + void retain(int length, [Attributes? attributes]) => + add(TextRetain(length, attributes)); - Delta delete(int length) { - final op = TextDelete(length); - return add(op); - } + void delete(int length) => add(TextDelete(length)); int get length { return _operations.fold( @@ -409,7 +400,7 @@ class Delta extends Iterable { if (!otherIter.hasNext && delta._operations[delta._operations.length - 1] == newOp) { final rest = Delta(thisIter.rest()); - return delta.concat(rest).chop(); + return (delta + rest)..chop(); } } else if (otherOp is TextDelete && (thisOp is TextRetain)) { delta.add(otherOp); @@ -417,10 +408,10 @@ class Delta extends Iterable { } } - return delta.chop(); + return delta..chop(); } - Delta concat(Delta other) { + Delta operator +(Delta other) { var ops = [..._operations]; if (other._operations.isNotEmpty) { ops.add(other._operations[0]); @@ -429,16 +420,15 @@ class Delta extends Iterable { return Delta(ops); } - Delta chop() { + void chop() { if (_operations.isEmpty) { - return this; + return; } _rawString = null; final lastOp = _operations.last; if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { _operations.removeLast(); } - return this; } @override @@ -477,7 +467,7 @@ class Delta extends Iterable { } return previousValue; }); - return inverted.chop(); + return inverted..chop(); } List toJson() { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index 918f68118d..305791b519 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -113,22 +113,32 @@ class TransactionBuilder { } textEdit( node, - () => Delta().retain(index).insert( - content, - newAttributes, - ), + () => Delta() + ..retain(index) + ..insert( + content, + newAttributes, + ), ); afterSelection = Selection.collapsed( Position(path: node.path, offset: index + content.length)); } formatText(TextNode node, int index, int length, Attributes attributes) { - textEdit(node, () => Delta().retain(index).retain(length, attributes)); + textEdit( + node, + () => Delta() + ..retain(index) + ..retain(length, attributes)); afterSelection = beforeSelection; } deleteText(TextNode node, int index, int length) { - textEdit(node, () => Delta().retain(index).delete(length)); + textEdit( + node, + () => Delta() + ..retain(index) + ..delete(length)); afterSelection = Selection.collapsed(Position(path: node.path, offset: index)); } @@ -144,7 +154,10 @@ class TransactionBuilder { } textEdit( node, - () => Delta().retain(index).delete(length).insert(content, newAttributes), + () => Delta() + ..retain(index) + ..delete(length) + ..insert(content, newAttributes), ); afterSelection = Selection.collapsed( Position( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index debce245c2..0b87fc0fd4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -67,7 +67,7 @@ _pasteHTML(EditorState editorState, String html) { final textNodeAtPath = nodeAtPath as TextNode; final firstTextNode = firstNode as TextNode; tb.textEdit(textNodeAtPath, - () => Delta().retain(startOffset).concat(firstTextNode.delta)); + () => (Delta()..retain(startOffset)) + firstTextNode.delta); tb.setAfterSelection(Selection.collapsed(Position( path: path, offset: startOffset + firstTextNode.delta.length))); tb.commit(); @@ -93,17 +93,18 @@ _pasteMultipleLinesInText( tb.textEdit( textNodeAtPath, - () => Delta() - .retain(offset) - .delete(remain.length) - .concat(firstTextNode.delta)); + () => + (Delta() + ..retain(offset) + ..delete(remain.length)) + + firstTextNode.delta); final tailNodes = nodes.sublist(1); path[path.length - 1]++; if (tailNodes.isNotEmpty) { if (tailNodes.last.type == "text") { final tailTextNode = tailNodes.last as TextNode; - tailTextNode.delta = tailTextNode.delta.concat(remain); + tailTextNode.delta = tailTextNode.delta + remain; } else if (remain.length > 0) { tailNodes.add(TextNode(type: "text", delta: remain)); } @@ -151,7 +152,11 @@ _handlePastePlainText(EditorState editorState, String plainText) { editorState.document.nodeAtPath(selection.end.path)! as TextNode; final beginOffset = selection.end.offset; TransactionBuilder(editorState) - ..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0])) + ..textEdit( + node, + () => Delta() + ..retain(beginOffset) + ..insert(lines[0])) ..setAfterSelection(Selection.collapsed(Position( path: selection.end.path, offset: beginOffset + lines[0].length))) ..commit(); @@ -175,17 +180,20 @@ _handlePastePlainText(EditorState editorState, String plainText) { final nodes = remains.map((e) { if (index++ == remains.length - 1) { return TextNode( - type: "text", delta: Delta().insert(e).addAll(insertedLineSuffix)); + type: "text", + delta: Delta() + ..insert(e) + ..addAll(insertedLineSuffix)); } - return TextNode(type: "text", delta: Delta().insert(e)); + return TextNode(type: "text", delta: Delta()..insert(e)); }).toList(); // insert first line tb.textEdit( node, () => Delta() - .retain(beginOffset) - .insert(firstLine) - .delete(node.delta.length - beginOffset)); + ..retain(beginOffset) + ..insert(firstLine) + ..delete(node.delta.length - beginOffset)); // insert remains tb.insertNodes(path, nodes); tb.commit(); @@ -226,7 +234,10 @@ _deleteSelectedContent(EditorState editorState) { final tb = TransactionBuilder(editorState); final len = selection.end.offset - selection.start.offset; tb.textEdit( - textItem, () => Delta().retain(selection.start.offset).delete(len)); + textItem, + () => Delta() + ..retain(selection.start.offset) + ..delete(len)); tb.setAfterSelection(Selection.collapsed(selection.start)); tb.commit(); return; @@ -240,8 +251,9 @@ _deleteSelectedContent(EditorState editorState) { final textItem = item as TextNode; final deleteLen = textItem.delta.length - selection.start.offset; tb.textEdit(textItem, () { - final delta = Delta(); - delta.retain(selection.start.offset).delete(deleteLen); + final delta = Delta() + ..retain(selection.start.offset) + ..delete(deleteLen); if (endNode is TextNode) { final remain = endNode.delta.slice(selection.end.offset); diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index c62f600266..7016da70e4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -14,9 +14,12 @@ void main() { }) ]); - final death = Delta().retain(12).insert("White", { - 'color': '#fff', - }).delete(4); + final death = Delta() + ..retain(12) + ..insert("White", { + 'color': '#fff', + }) + ..delete(4); final restores = delta.compose(death); expect(restores.toList(), [ @@ -26,164 +29,203 @@ void main() { ]); }); test('compose()', () { - final a = Delta().insert('A'); - final b = Delta().insert('B'); - final expected = Delta().insert('B').insert('A'); + final a = Delta()..insert('A'); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..insert('A'); expect(a.compose(b), expected); }); test('insert + retain', () { - final a = Delta().insert('A'); - final b = Delta().retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta().insert('A', { - 'bold': true, - 'color': 'red', - }); + final a = Delta()..insert('A'); + final b = Delta() + ..retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..insert('A', { + 'bold': true, + 'color': 'red', + }); expect(a.compose(b), expected); }); test('insert + delete', () { - final a = Delta().insert('A'); - final b = Delta().delete(1); + final a = Delta()..insert('A'); + final b = Delta()..delete(1); final expected = Delta(); expect(a.compose(b), expected); }); test('delete + insert', () { - final a = Delta().delete(1); - final b = Delta().insert('B'); - final expected = Delta().insert('B').delete(1); + final a = Delta()..delete(1); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..delete(1); expect(a.compose(b), expected); }); test('delete + retain', () { - final a = Delta().delete(1); - final b = Delta().retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta().delete(1).retain(1, { - 'bold': true, - 'color': 'red', - }); + final a = Delta()..delete(1); + final b = Delta() + ..retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..delete(1) + ..retain(1, { + 'bold': true, + 'color': 'red', + }); expect(a.compose(b), expected); }); test('delete + delete', () { - final a = Delta().delete(1); - final b = Delta().delete(1); - final expected = Delta().delete(2); + final a = Delta()..delete(1); + final b = Delta()..delete(1); + final expected = Delta()..delete(2); expect(a.compose(b), expected); }); test('retain + insert', () { - final a = Delta().retain(1, {'color': 'blue'}); - final b = Delta().insert('B'); - final expected = Delta().insert('B').retain(1, { - 'color': 'blue', - }); + final a = Delta()..retain(1, {'color': 'blue'}); + final b = Delta()..insert('B'); + final expected = Delta() + ..insert('B') + ..retain(1, { + 'color': 'blue', + }); expect(a.compose(b), expected); }); test('retain + retain', () { - final a = Delta().retain(1, { - 'color': 'blue', - }); - final b = Delta().retain(1, { - 'bold': true, - 'color': 'red', - }); - final expected = Delta().retain(1, { - 'bold': true, - 'color': 'red', - }); + final a = Delta() + ..retain(1, { + 'color': 'blue', + }); + final b = Delta() + ..retain(1, { + 'bold': true, + 'color': 'red', + }); + final expected = Delta() + ..retain(1, { + 'bold': true, + 'color': 'red', + }); expect(a.compose(b), expected); }); test('retain + delete', () { - final a = Delta().retain(1, { - 'color': 'blue', - }); - final b = Delta().delete(1); - final expected = Delta().delete(1); + final a = Delta() + ..retain(1, { + 'color': 'blue', + }); + final b = Delta()..delete(1); + final expected = Delta()..delete(1); expect(a.compose(b), expected); }); test('insert in middle of text', () { - final a = Delta().insert('Hello'); - final b = Delta().retain(3).insert('X'); - final expected = Delta().insert('HelXlo'); + final a = Delta()..insert('Hello'); + final b = Delta() + ..retain(3) + ..insert('X'); + final expected = Delta()..insert('HelXlo'); expect(a.compose(b), expected); }); test('insert and delete ordering', () { - final a = Delta().insert('Hello'); - final b = Delta().insert('Hello'); - final insertFirst = Delta().retain(3).insert('X').delete(1); - final deleteFirst = Delta().retain(3).delete(1).insert('X'); - final expected = Delta().insert('HelXo'); + final a = Delta()..insert('Hello'); + final b = Delta()..insert('Hello'); + final insertFirst = Delta() + ..retain(3) + ..insert('X') + ..delete(1); + final deleteFirst = Delta() + ..retain(3) + ..delete(1) + ..insert('X'); + final expected = Delta()..insert('HelXo'); expect(a.compose(insertFirst), expected); expect(b.compose(deleteFirst), expected); }); test('delete entire text', () { - final a = Delta().retain(4).insert('Hello'); - final b = Delta().delete(9); - final expected = Delta().delete(4); + final a = Delta() + ..retain(4) + ..insert('Hello'); + final b = Delta()..delete(9); + final expected = Delta()..delete(4); expect(a.compose(b), expected); }); test('retain more than length of text', () { - final a = Delta().insert('Hello'); - final b = Delta().retain(10); - final expected = Delta().insert('Hello'); + final a = Delta()..insert('Hello'); + final b = Delta()..retain(10); + final expected = Delta()..insert('Hello'); expect(a.compose(b), expected); }); test('retain start optimization', () { final a = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}) - .delete(1); - final b = Delta().retain(3).insert('D'); + ..insert('A', {'bold': true}) + ..insert('B') + ..insert('C', {'bold': true}) + ..delete(1); + final b = Delta() + ..retain(3) + ..insert('D'); final expected = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}) - .insert('D') - .delete(1); + ..insert('A', {'bold': true}) + ..insert('B') + ..insert('C', {'bold': true}) + ..insert('D') + ..delete(1); expect(a.compose(b), expected); }); test('retain end optimization', () { final a = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}); - final b = Delta().delete(1); - final expected = Delta().insert('B').insert('C', {'bold': true}); + ..insert('A', {'bold': true}) + ..insert('B') + ..insert('C', {'bold': true}); + final b = Delta()..delete(1); + final expected = Delta() + ..insert('B') + ..insert('C', {'bold': true}); expect(a.compose(b), expected); }); test('retain end optimization join', () { final a = Delta() - .insert('A', {'bold': true}) - .insert('B') - .insert('C', {'bold': true}) - .insert('D') - .insert('E', {'bold': true}) - .insert('F'); - final b = Delta().retain(1).delete(1); + ..insert('A', {'bold': true}) + ..insert('B') + ..insert('C', {'bold': true}) + ..insert('D') + ..insert('E', {'bold': true}) + ..insert('F'); + final b = Delta() + ..retain(1) + ..delete(1); final expected = Delta() - .insert('AC', {'bold': true}) - .insert('D') - .insert('E', {'bold': true}) - .insert('F'); + ..insert('AC', {'bold': true}) + ..insert('D') + ..insert('E', {'bold': true}) + ..insert('F'); expect(a.compose(b), expected); }); }); group('invert', () { test('insert', () { - final delta = Delta().retain(2).insert('A'); - final base = Delta().insert('12346'); - final expected = Delta().retain(2).delete(1); + final delta = Delta() + ..retain(2) + ..insert('A'); + final base = Delta()..insert('12346'); + final expected = Delta() + ..retain(2) + ..delete(1); final inverted = delta.invert(base); expect(expected, inverted); expect(base.compose(delta).compose(inverted), base); }); test('delete', () { - final delta = Delta().retain(2).delete(3); - final base = Delta().insert('123456'); - final expected = Delta().retain(2).insert('345'); + final delta = Delta() + ..retain(2) + ..delete(3); + final base = Delta()..insert('123456'); + final expected = Delta() + ..retain(2) + ..insert('345'); final inverted = delta.invert(base); expect(expected, inverted); expect(base.compose(delta).compose(inverted), base); @@ -199,7 +241,10 @@ void main() { }); group('json', () { test('toJson()', () { - final delta = Delta().retain(2).insert('A').delete(3); + final delta = Delta() + ..retain(2) + ..insert('A') + ..delete(3); expect(delta.toJson(), [ {'retain': 2}, {'insert': 'A'}, @@ -207,8 +252,9 @@ void main() { ]); }); test('attributes', () { - final delta = - Delta().retain(2, {'bold': true}).insert('A', {'italic': true}); + final delta = Delta() + ..retain(2, {'bold': true}) + ..insert('A', {'italic': true}); expect(delta.toJson(), [ { 'retain': 2, @@ -226,7 +272,10 @@ void main() { {'insert': 'A'}, {'delete': 3}, ]); - final expected = Delta().retain(2).insert('A').delete(3); + final expected = Delta() + ..retain(2) + ..insert('A') + ..delete(3); expect(delta, expected); }); }); From bee1a0432936c228a15866e64b3696f40b99371b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 17:24:39 +0800 Subject: [PATCH 051/224] feat: compute string indexes --- .../flowy_editor/lib/src/document/text_delta.dart | 15 +++++++++++++++ .../packages/flowy_editor/test/delta_test.dart | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 6626ba7a86..03ae4ccc56 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -260,6 +260,7 @@ TextOperation? _textOperationFromJson(Map json) { class Delta extends Iterable { final List _operations; String? _rawString; + List? _runeIndexes; factory Delta.fromJson(List list) { final operations = []; @@ -477,9 +478,23 @@ class Delta extends Iterable { String toRawString() { _rawString ??= _operations.whereType().map((op) => op.content).join(); + _runeIndexes ??= stringIndexes(_rawString!); return _rawString!; } @override Iterator get iterator => _operations.iterator; } + +List stringIndexes(String content) { + final indexes = List.filled(content.length, 0); + final iterator = content.runes.iterator; + + while (iterator.moveNext()) { + for (var i = 0; i < iterator.currentSize; i++) { + indexes[iterator.rawIndex + i] = iterator.rawIndex; + } + } + + return indexes; +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 7016da70e4..2b0d529414 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -279,4 +279,9 @@ void main() { expect(delta, expected); }); }); + test("stringIndexes", () { + final indexes = stringIndexes('😊'); + expect(indexes[0], 0); + expect(indexes[1], 0); + }); } From c554720dffe207ba77a19b31377f930de214db5e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 17:27:23 +0800 Subject: [PATCH 052/224] fix: emoji position --- .../lib/src/document/text_delta.dart | 27 ++++++++++++++++++- .../arrow_keys_handler.dart | 14 +++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 03ae4ccc56..b1e6525c49 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -475,10 +475,35 @@ class Delta extends Iterable { return _operations.map((e) => e.toJson()).toList(); } - String toRawString() { + int prevRunePosition(int pos) { + if (pos == 0) { + return pos; + } _rawString ??= _operations.whereType().map((op) => op.content).join(); _runeIndexes ??= stringIndexes(_rawString!); + return _runeIndexes![pos - 1]; + } + + int nextRunePosition(int pos) { + final stringContent = toRawString(); + if (pos >= stringContent.length - 1) { + return stringContent.length; + } + _runeIndexes ??= stringIndexes(_rawString!); + + for (var i = pos + 1; i < _runeIndexes!.length; i++) { + if (_runeIndexes![i] != pos) { + return _runeIndexes![i]; + } + } + + return pos; + } + + String toRawString() { + _rawString ??= + _operations.whereType().map((op) => op.content).join(); return _rawString!; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index 83243d2dc7..826308e565 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -12,8 +12,8 @@ int _endOffsetOfNode(Node node) { extension on Position { Position? goLeft(EditorState editorState) { + final node = editorState.document.nodeAtPath(path)!; if (offset == 0) { - final node = editorState.document.nodeAtPath(path)!; final prevNode = node.previous; if (prevNode != null) { return Position( @@ -22,7 +22,11 @@ extension on Position { return null; } - return Position(path: path, offset: offset - 1); + if (node is TextNode) { + return Position(path: path, offset: node.delta.prevRunePosition(offset)); + } else { + return Position(path: path, offset: offset); + } } Position? goRight(EditorState editorState) { @@ -36,7 +40,11 @@ extension on Position { return null; } - return Position(path: path, offset: offset + 1); + if (node is TextNode) { + return Position(path: path, offset: node.delta.nextRunePosition(offset)); + } else { + return Position(path: path, offset: offset); + } } } From ad3e2f57253f45f7bb7ecee006c932f9f5f2878c Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 17:59:28 +0800 Subject: [PATCH 053/224] chore: add sub data type --- .../plugins/board/application/board_bloc.dart | 34 ++++-------- .../board/application/field_group.dart | 0 .../app_flowy/lib/plugins/board/board.dart | 5 +- .../board/presentation/board_page.dart | 53 ++++--------------- .../lib/plugins/board/presentation/card.dart | 13 +++++ .../app_flowy/lib/plugins/doc/document.dart | 6 +-- frontend/app_flowy/lib/plugins/grid/grid.dart | 5 +- .../app_flowy/lib/startup/plugin/plugin.dart | 9 ++-- .../workspace/application/app/app_bloc.dart | 4 +- .../application/app/app_service.dart | 9 +++- .../home/menu/app/header/header.dart | 18 +++++-- .../flowy-folder/src/entities/view.rs | 41 ++++++++------ .../flowy-folder/src/entities/view_info.rs | 4 +- frontend/rust-lib/flowy-folder/src/manager.rs | 16 ++++-- .../persistence/version_1/view_sql.rs | 4 +- .../src/services/view/controller.rs | 15 +++--- .../tests/workspace/folder_test.rs | 14 ++--- .../flowy-folder/tests/workspace/script.rs | 14 +++-- .../single_select_type_option.rs | 8 +-- .../text_type_option/text_type_option.rs | 2 +- .../flowy-grid/src/services/grid_editor.rs | 2 +- .../src/services/row/row_builder.rs | 25 +++++---- frontend/rust-lib/flowy-grid/src/util.rs | 45 ++++++++++++++++ .../flowy-grid/tests/grid/block_test/util.rs | 10 ++-- .../flowy-grid/tests/grid/field_test/util.rs | 4 +- .../flowy-grid/tests/grid/grid_editor.rs | 6 +-- .../flowy-sdk/src/deps_resolve/folder_deps.rs | 33 ++++++++---- frontend/rust-lib/flowy-test/src/helper.rs | 8 +-- .../src/revision/view_rev.rs | 2 +- 29 files changed, 243 insertions(+), 166 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/field_group.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index a2ce5e043f..2cb03a6c64 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -6,6 +6,7 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/b import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; +import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; @@ -55,15 +56,6 @@ class BoardBloc extends Bloc { didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - didReceiveFieldUpdate: (UnmodifiableListView fields) { - emit(state.copyWith(fields: GridFieldEquatable(fields))); - }, - didReceiveRowUpdate: ( - List newRowInfos, - GridRowChangeReason reason, - ) { - emit(state.copyWith(rowInfos: newRowInfos, reason: reason)); - }, ); }, ); @@ -89,18 +81,21 @@ class BoardBloc extends Bloc { }, onRowsChanged: (rowInfos, reason) { if (!isClosed) { - add(BoardEvent.didReceiveRowUpdate(rowInfos, reason)); + _buildColumnItems(rowInfos); } }, onFieldsChanged: (fields) { if (!isClosed) { _buildColumns(fields); - add(BoardEvent.didReceiveFieldUpdate(fields)); } }, ); } + void _buildColumnItems(List rowInfos) { + for (final rowInfo in rowInfos) {} + } + void _buildColumns(UnmodifiableListView fields) { for (final field in fields) { if (field.fieldType == FieldType.SingleSelect) { @@ -127,7 +122,7 @@ class BoardBloc extends Bloc { boardDataController.addColumns(columns); }, - onError: (err) {}, + onError: (err) => Log.error(err), ); } @@ -148,14 +143,7 @@ class BoardBloc extends Bloc { class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = InitialGrid; const factory BoardEvent.createRow() = _CreateRow; - const factory BoardEvent.didReceiveRowUpdate( - List rows, - GridRowChangeReason listState, - ) = _DidReceiveRowUpdate; - const factory BoardEvent.didReceiveFieldUpdate( - UnmodifiableListView fields, - ) = _DidReceiveFieldUpdate; - + const factory BoardEvent.groupByField(GridFieldPB field) = _GroupByField; const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; @@ -166,19 +154,17 @@ class BoardState with _$BoardState { const factory BoardState({ required String gridId, required Option grid, - required GridFieldEquatable fields, + required Option groupField, required List rowInfos, required GridLoadingState loadingState, - required GridRowChangeReason reason, }) = _BoardState; factory BoardState.initial(String gridId) => BoardState( - fields: GridFieldEquatable(UnmodifiableListView([])), rowInfos: [], + groupField: none(), grid: none(), gridId: gridId, loadingState: const _Loading(), - reason: const InitialListState(), ); } diff --git a/frontend/app_flowy/lib/plugins/board/application/field_group.dart b/frontend/app_flowy/lib/plugins/board/application/field_group.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 11a6d415eb..36d181ae3e 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -23,7 +23,10 @@ class BoardPluginBuilder implements PluginBuilder { PluginType get pluginType => DefaultPlugin.board.type(); @override - ViewDataType get dataType => ViewDataType.Grid; + ViewDataTypePB get dataType => ViewDataTypePB.Database; + + @override + SubViewDataTypePB get subDataType => SubViewDataTypePB.Board; } class BoardPluginConfig implements PluginConfig { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index f961eeaf42..1155168e70 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,15 +1,17 @@ // ignore_for_file: unused_field +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/board_bloc.dart'; +import 'card.dart'; class BoardPage extends StatelessWidget { final ViewPB view; - const BoardPage({required this.view, Key? key}) : super(key: key); + BoardPage({required this.view, Key? key}) : super(key: ValueKey(view.id)); @override Widget build(BuildContext context) { @@ -53,12 +55,7 @@ class BoardContent extends StatelessWidget { dataController: context.read().boardDataController, headerBuilder: _buildHeader, footBuilder: _buildFooter, - cardBuilder: (context, item) { - return AppFlowyColumnItemCard( - key: ObjectKey(item), - child: _buildCard(item), - ); - }, + cardBuilder: _buildCard, columnConstraints: const BoxConstraints.tightFor(width: 240), config: BoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), @@ -90,42 +87,12 @@ class BoardContent extends StatelessWidget { ); } - Widget _buildCard(ColumnItem item) { - if (item is TextItem) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text(item.s), - ), - ); - } - - if (item is RichTextItem) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - style: const TextStyle(fontSize: 14), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - Text( - item.subtitle, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ) - ], - ), - ), - ); - } - - throw UnimplementedError(); + Widget _buildCard(BuildContext context, ColumnItem item) { + final rowInfo = item as GridRowInfo; + return AppFlowyColumnItemCard( + key: ObjectKey(item), + child: BoardCard(rowInfo: rowInfo), + ); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card.dart new file mode 100644 index 0000000000..b98739eea8 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card.dart @@ -0,0 +1,13 @@ +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:flutter/material.dart'; + +class BoardCard extends StatelessWidget { + final GridRowInfo rowInfo; + + const BoardCard({required this.rowInfo, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(child: Text('1234')); + } +} diff --git a/frontend/app_flowy/lib/plugins/doc/document.dart b/frontend/app_flowy/lib/plugins/doc/document.dart index 91bae222c1..0deaedbb75 100644 --- a/frontend/app_flowy/lib/plugins/doc/document.dart +++ b/frontend/app_flowy/lib/plugins/doc/document.dart @@ -1,4 +1,4 @@ -library docuemnt_plugin; +library document_plugin; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/startup/plugin/plugin.dart'; @@ -42,10 +42,10 @@ class DocumentPluginBuilder extends PluginBuilder { String get menuName => LocaleKeys.document_menuName.tr(); @override - PluginType get pluginType => DefaultPlugin.quill.type(); + PluginType get pluginType => DefaultPlugin.editor.type(); @override - ViewDataType get dataType => ViewDataType.TextBlock; + ViewDataTypePB get dataType => ViewDataTypePB.TextBlock; } class DocumentPlugin implements Plugin { diff --git a/frontend/app_flowy/lib/plugins/grid/grid.dart b/frontend/app_flowy/lib/plugins/grid/grid.dart index c0c6d78642..9ac539929a 100644 --- a/frontend/app_flowy/lib/plugins/grid/grid.dart +++ b/frontend/app_flowy/lib/plugins/grid/grid.dart @@ -25,7 +25,10 @@ class GridPluginBuilder implements PluginBuilder { PluginType get pluginType => DefaultPlugin.grid.type(); @override - ViewDataType get dataType => ViewDataType.Grid; + ViewDataTypePB get dataType => ViewDataTypePB.Database; + + @override + SubViewDataTypePB? get subDataType => SubViewDataTypePB.Grid; } class GridPluginConfig implements PluginConfig { diff --git a/frontend/app_flowy/lib/startup/plugin/plugin.dart b/frontend/app_flowy/lib/startup/plugin/plugin.dart index a02a8cfb83..2879af42cb 100644 --- a/frontend/app_flowy/lib/startup/plugin/plugin.dart +++ b/frontend/app_flowy/lib/startup/plugin/plugin.dart @@ -10,7 +10,7 @@ import 'package:flutter/widgets.dart'; export "./src/sandbox.dart"; enum DefaultPlugin { - quill, + editor, blank, trash, grid, @@ -20,7 +20,7 @@ enum DefaultPlugin { extension FlowyDefaultPluginExt on DefaultPlugin { int type() { switch (this) { - case DefaultPlugin.quill: + case DefaultPlugin.editor: return 0; case DefaultPlugin.blank: return 1; @@ -35,7 +35,6 @@ extension FlowyDefaultPluginExt on DefaultPlugin { } typedef PluginType = int; -typedef PluginDataType = ViewDataType; typedef PluginId = String; abstract class Plugin { @@ -55,7 +54,9 @@ abstract class PluginBuilder { PluginType get pluginType; - ViewDataType get dataType => ViewDataType.TextBlock; + ViewDataTypePB get dataType => ViewDataTypePB.TextBlock; + + SubViewDataTypePB? get subDataType => null; } abstract class PluginConfig { diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart index f86bb5855b..c593c52679 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart @@ -86,6 +86,7 @@ class AppBloc extends Bloc { desc: value.desc, dataType: value.dataType, pluginType: value.pluginType, + subDataType: value.subDataType, ); viewOrFailed.fold( (view) => emit(state.copyWith( @@ -138,7 +139,8 @@ class AppEvent with _$AppEvent { const factory AppEvent.createView( String name, String desc, - PluginDataType dataType, + ViewDataTypePB dataType, + SubViewDataTypePB? subDataType, PluginType pluginType, ) = CreateView; const factory AppEvent.delete() = Delete; diff --git a/frontend/app_flowy/lib/workspace/application/app/app_service.dart b/frontend/app_flowy/lib/workspace/application/app/app_service.dart index 695f2f4b6b..b05c64dd23 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_service.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_service.dart @@ -24,16 +24,21 @@ class AppService { required String appId, required String name, required String desc, - required PluginDataType dataType, + required ViewDataTypePB dataType, required PluginType pluginType, + SubViewDataTypePB? subDataType, }) { - final payload = CreateViewPayloadPB.create() + var payload = CreateViewPayloadPB.create() ..belongToId = appId ..name = name ..desc = desc ..dataType = dataType ..pluginType = pluginType; + if (subDataType != null) { + payload.subDataType = subDataType; + } + return FolderEventCreateView(payload).send(); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart index dbeb2248cc..0cfac08628 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart @@ -49,7 +49,9 @@ class MenuAppHeader extends StatelessWidget { height: MenuAppSizes.headerHeight, child: InkWell( onTap: () { - ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle(); + ExpandableController.of(context, + rebuildOnChange: false, required: true) + ?.toggle(); }, child: ExpandableIcon( theme: ExpandableThemeData( @@ -68,18 +70,23 @@ class MenuAppHeader extends StatelessWidget { Widget _renderTitle(BuildContext context, AppTheme theme) { return Expanded( child: BlocListener( - listenWhen: (p, c) => (p.latestCreatedView == null && c.latestCreatedView != null), + listenWhen: (p, c) => + (p.latestCreatedView == null && c.latestCreatedView != null), listener: (context, state) { - final expandableController = ExpandableController.of(context, rebuildOnChange: false, required: true)!; + final expandableController = ExpandableController.of(context, + rebuildOnChange: false, required: true)!; if (!expandableController.expanded) { expandableController.toggle(); } }, child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => ExpandableController.of(context, rebuildOnChange: false, required: true)?.toggle(), + onTap: () => ExpandableController.of(context, + rebuildOnChange: false, required: true) + ?.toggle(), onSecondaryTap: () { - final actionList = AppDisclosureActionSheet(onSelected: (action) => _handleAction(context, action)); + final actionList = AppDisclosureActionSheet( + onSelected: (action) => _handleAction(context, action)); actionList.show( context, anchorDirection: AnchorDirection.bottomWithCenterAligned, @@ -107,6 +114,7 @@ class MenuAppHeader extends StatelessWidget { LocaleKeys.menuAppHeader_defaultNewPageName.tr(), "", pluginBuilder.dataType, + pluginBuilder.subDataType, pluginBuilder.pluginType, )); }, diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 8f3a26c8fd..ae77017947 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -22,7 +22,7 @@ pub struct ViewPB { pub name: String, #[pb(index = 4)] - pub data_type: ViewDataType, + pub data_type: ViewDataTypePB, #[pb(index = 5)] pub modified_time: i64, @@ -49,31 +49,37 @@ impl std::convert::From for ViewPB { } #[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)] -pub enum ViewDataType { +pub enum ViewDataTypePB { TextBlock = 0, - Grid = 1, + Database = 1, } -impl std::default::Default for ViewDataType { +#[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone)] +pub enum SubViewDataTypePB { + Grid = 0, + Board = 1, +} + +impl std::default::Default for ViewDataTypePB { fn default() -> Self { ViewDataTypeRevision::default().into() } } -impl std::convert::From for ViewDataType { +impl std::convert::From for ViewDataTypePB { fn from(rev: ViewDataTypeRevision) -> Self { match rev { - ViewDataTypeRevision::TextBlock => ViewDataType::TextBlock, - ViewDataTypeRevision::Grid => ViewDataType::Grid, + ViewDataTypeRevision::TextBlock => ViewDataTypePB::TextBlock, + ViewDataTypeRevision::Database => ViewDataTypePB::Database, } } } -impl std::convert::From for ViewDataTypeRevision { - fn from(ty: ViewDataType) -> Self { +impl std::convert::From for ViewDataTypeRevision { + fn from(ty: ViewDataTypePB) -> Self { match ty { - ViewDataType::TextBlock => ViewDataTypeRevision::TextBlock, - ViewDataType::Grid => ViewDataTypeRevision::Grid, + ViewDataTypePB::TextBlock => ViewDataTypeRevision::TextBlock, + ViewDataTypePB::Database => ViewDataTypeRevision::Database, } } } @@ -113,12 +119,15 @@ pub struct CreateViewPayloadPB { pub thumbnail: Option, #[pb(index = 5)] - pub data_type: ViewDataType, + pub data_type: ViewDataTypePB, - #[pb(index = 6)] - pub plugin_type: i32, + #[pb(index = 6, one_of)] + pub sub_data_type: Option, #[pb(index = 7)] + pub plugin_type: i32, + + #[pb(index = 8)] pub data: Vec, } @@ -128,7 +137,8 @@ pub struct CreateViewParams { pub name: String, pub desc: String, pub thumbnail: String, - pub data_type: ViewDataType, + pub data_type: ViewDataTypePB, + pub sub_data_type: Option, pub view_id: String, pub data: Vec, pub plugin_type: i32, @@ -151,6 +161,7 @@ impl TryInto for CreateViewPayloadPB { name, desc: self.desc, data_type: self.data_type, + sub_data_type: self.sub_data_type, thumbnail, view_id, data: self.data, diff --git a/frontend/rust-lib/flowy-folder/src/entities/view_info.rs b/frontend/rust-lib/flowy-folder/src/entities/view_info.rs index 92ec785821..42dbc42517 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view_info.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view_info.rs @@ -1,4 +1,4 @@ -use crate::entities::{RepeatedViewPB, ViewDataType}; +use crate::entities::{RepeatedViewPB, ViewDataTypePB}; use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -16,7 +16,7 @@ pub struct ViewInfoPB { pub desc: String, #[pb(index = 5)] - pub data_type: ViewDataType, + pub data_type: ViewDataTypePB, #[pb(index = 6)] pub belongings: RepeatedViewPB, diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index df070d90f3..96e35ba547 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,4 +1,5 @@ -use crate::entities::view::ViewDataType; +use crate::entities::view::ViewDataTypePB; +use crate::entities::SubViewDataTypePB; use crate::services::folder_editor::FolderRevisionCompactor; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, @@ -221,7 +222,7 @@ impl DefaultFolderBuilder { }; let _ = view_controller.set_latest_view(&view.id); let _ = view_controller - .create_view(&view.id, ViewDataType::TextBlock, Bytes::from(view_data)) + .create_view(&view.id, ViewDataTypePB::TextBlock, Bytes::from(view_data)) .await?; } } @@ -256,7 +257,12 @@ pub trait ViewDataProcessor { fn get_delta_data(&self, view_id: &str) -> FutureResult; - fn create_default_view(&self, user_id: &str, view_id: &str) -> FutureResult; + fn create_default_view( + &self, + user_id: &str, + view_id: &str, + sub_data_type: Option, + ) -> FutureResult; fn create_view_from_delta_data( &self, @@ -265,7 +271,7 @@ pub trait ViewDataProcessor { data: Vec, ) -> FutureResult; - fn data_type(&self) -> ViewDataType; + fn data_type(&self) -> ViewDataTypePB; } -pub type ViewDataProcessorMap = Arc>>; +pub type ViewDataProcessorMap = Arc>>; diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs index 6223cd3366..245bd32f83 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs @@ -88,7 +88,7 @@ impl ViewTable { pub fn new(view_rev: ViewRevision) -> Self { let data_type = match view_rev.data_type { ViewDataTypeRevision::TextBlock => SqlViewDataType::Block, - ViewDataTypeRevision::Grid => SqlViewDataType::Grid, + ViewDataTypeRevision::Database => SqlViewDataType::Grid, }; ViewTable { @@ -111,7 +111,7 @@ impl std::convert::From for ViewRevision { fn from(table: ViewTable) -> Self { let data_type = match table.view_type { SqlViewDataType::Block => ViewDataTypeRevision::TextBlock, - SqlViewDataType::Grid => ViewDataTypeRevision::Grid, + SqlViewDataType::Grid => ViewDataTypeRevision::Database, }; ViewRevision { diff --git a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs index df1f668941..defbe664db 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs @@ -1,5 +1,5 @@ -pub use crate::entities::view::ViewDataType; -use crate::entities::ViewInfoPB; +pub use crate::entities::view::ViewDataTypePB; +use crate::entities::{SubViewDataTypePB, ViewInfoPB}; use crate::manager::{ViewDataProcessor, ViewDataProcessorMap}; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, @@ -61,7 +61,9 @@ impl ViewController { let processor = self.get_data_processor(params.data_type.clone())?; let user_id = self.user.user_id()?; if params.data.is_empty() { - let view_data = processor.create_default_view(&user_id, ¶ms.view_id).await?; + let view_data = processor + .create_default_view(&user_id, ¶ms.view_id, params.sub_data_type.clone()) + .await?; params.data = view_data.to_vec(); } else { let delta_data = processor @@ -81,7 +83,7 @@ impl ViewController { pub(crate) async fn create_view( &self, view_id: &str, - data_type: ViewDataType, + data_type: ViewDataTypePB, delta_data: Bytes, ) -> Result<(), FlowyError> { if delta_data.is_empty() { @@ -217,6 +219,7 @@ impl ViewController { desc: view_rev.desc, thumbnail: view_rev.thumbnail, data_type: view_rev.data_type.into(), + sub_data_type: None, data: delta_bytes.to_vec(), view_id: gen_view_id(), plugin_type: view_rev.plugin_type, @@ -364,7 +367,7 @@ impl ViewController { } #[inline] - fn get_data_processor>( + fn get_data_processor>( &self, data_type: T, ) -> FlowyResult> { @@ -452,7 +455,7 @@ async fn handle_trash_event( fn get_data_processor( data_processors: ViewDataProcessorMap, - data_type: &ViewDataType, + data_type: &ViewDataTypePB, ) -> FlowyResult> { match data_processors.get(data_type) { None => Err(FlowyError::internal().context(format!( diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs b/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs index f17986b484..15af0cb8d9 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs @@ -1,5 +1,5 @@ use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest}; -use flowy_folder::entities::view::ViewDataType; +use flowy_folder::entities::view::ViewDataTypePB; use flowy_folder::entities::workspace::CreateWorkspacePayloadPB; use flowy_revision::disk::RevisionState; @@ -134,12 +134,12 @@ async fn app_create_with_view() { CreateView { name: "View A".to_owned(), desc: "View A description".to_owned(), - data_type: ViewDataType::TextBlock, + data_type: ViewDataTypePB::TextBlock, }, CreateView { name: "Grid".to_owned(), desc: "Grid description".to_owned(), - data_type: ViewDataType::Grid, + data_type: ViewDataTypePB::Database, }, ReadApp(app.id), ]) @@ -198,12 +198,12 @@ async fn view_delete_all() { CreateView { name: "View A".to_owned(), desc: "View A description".to_owned(), - data_type: ViewDataType::TextBlock, + data_type: ViewDataTypePB::TextBlock, }, CreateView { name: "Grid".to_owned(), desc: "Grid description".to_owned(), - data_type: ViewDataType::Grid, + data_type: ViewDataTypePB::Database, }, ReadApp(app.id.clone()), ]) @@ -231,7 +231,7 @@ async fn view_delete_all_permanent() { CreateView { name: "View A".to_owned(), desc: "View A description".to_owned(), - data_type: ViewDataType::TextBlock, + data_type: ViewDataTypePB::TextBlock, }, ReadApp(app.id.clone()), ]) @@ -330,7 +330,7 @@ async fn folder_sync_revision_with_new_view() { CreateView { name: view_name.clone(), desc: view_desc.clone(), - data_type: ViewDataType::TextBlock, + data_type: ViewDataTypePB::TextBlock, }, AssertCurrentRevId(3), AssertNextSyncRevId(Some(3)), diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index ed16664d90..91fac9869e 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -9,7 +9,7 @@ use flowy_folder::entities::{ use flowy_folder::entities::{ app::{AppPB, RepeatedAppPB}, trash::TrashPB, - view::{RepeatedViewPB, ViewDataType, ViewPB}, + view::{RepeatedViewPB, ViewDataTypePB, ViewPB}, workspace::WorkspacePB, }; use flowy_folder::event_map::FolderEvent::*; @@ -51,7 +51,7 @@ pub enum FolderScript { CreateView { name: String, desc: String, - data_type: ViewDataType, + data_type: ViewDataTypePB, }, AssertView(ViewPB), ReadView(String), @@ -98,7 +98,7 @@ impl FolderTest { &app.id, "Folder View", "Folder test view", - ViewDataType::TextBlock, + ViewDataTypePB::TextBlock, ) .await; app.belongings = RepeatedViewPB { @@ -346,7 +346,13 @@ pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) { .await; } -pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, data_type: ViewDataType) -> ViewPB { +pub async fn create_view( + sdk: &FlowySDKTest, + app_id: &str, + name: &str, + desc: &str, + data_type: ViewDataTypePB, +) -> ViewPB { let request = CreateViewPayloadPB { belong_to_id: app_id.to_string(), name: name.to_string(), diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs index 553425222e..8722b3a479 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -82,7 +82,7 @@ impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder); impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOptionPB); impl SingleSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOptionPB) -> Self { + pub fn add_option(mut self, opt: SelectOptionPB) -> Self { self.0.options.push(opt); self } @@ -113,9 +113,9 @@ mod tests { let facebook_option = SelectOptionPB::new("Facebook"); let twitter_option = SelectOptionPB::new("Twitter"); let single_select = SingleSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()) - .option(twitter_option); + .add_option(google_option.clone()) + .add_option(facebook_option.clone()) + .add_option(twitter_option); let field_rev = FieldBuilder::new(single_select) .name("Platform") diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index b11bd026d2..43e2b009d0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -133,7 +133,7 @@ mod tests { // Single select let done_option = SelectOptionPB::new("Done"); let done_option_id = done_option.id.clone(); - let single_select = SingleSelectTypeOptionBuilder::default().option(done_option.clone()); + let single_select = SingleSelectTypeOptionBuilder::default().add_option(done_option.clone()); let single_select_field_rev = FieldBuilder::new(single_select).build(); assert_eq!( diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 44626b96fd..08c7284b74 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -272,7 +272,7 @@ impl GridRevisionEditor { let block_id = self.block_id().await?; // insert empty row below the row whose id is upper_row_id - let row_rev = RowRevisionBuilder::new(&field_revs).build(&block_id); + let row_rev = RowRevisionBuilder::new(&block_id, &field_revs).build(); let row_order = GridRowPB::from(&row_rev); // insert the row diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index 43153f1311..3973dd4aed 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -7,12 +7,13 @@ use std::collections::HashMap; use std::sync::Arc; pub struct RowRevisionBuilder<'a> { + block_id: String, field_rev_map: HashMap<&'a String, Arc>, payload: CreateRowRevisionPayload, } impl<'a> RowRevisionBuilder<'a> { - pub fn new(fields: &'a [Arc]) -> Self { + pub fn new(block_id: &str, fields: &'a [Arc]) -> Self { let field_rev_map = fields .iter() .map(|field| (&field.id, field.clone())) @@ -25,7 +26,13 @@ impl<'a> RowRevisionBuilder<'a> { visibility: true, }; - Self { field_rev_map, payload } + let block_id = block_id.to_string(); + + Self { + block_id, + field_rev_map, + payload, + } } pub fn insert_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { @@ -43,18 +50,18 @@ impl<'a> RowRevisionBuilder<'a> { } } - pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_select_option_cell(mut self, field_id: &str, data: String) -> Self { match self.field_rev_map.get(&field_id.to_owned()) { None => { - let msg = format!("Invalid field_id: {}", field_id); - Err(FlowyError::internal().context(msg)) + tracing::warn!("Invalid field_id: {}", field_id); + self } Some(field_rev) => { let cell_data = SelectOptionCellChangeset::from_insert(&data).to_str(); - let data = apply_cell_data_changeset(cell_data, None, field_rev)?; + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); let cell = CellRevision::new(data); self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); - Ok(()) + self } } } @@ -71,10 +78,10 @@ impl<'a> RowRevisionBuilder<'a> { self } - pub fn build(self, block_id: &str) -> RowRevision { + pub fn build(self) -> RowRevision { RowRevision { id: self.payload.row_id, - block_id: block_id.to_owned(), + block_id: self.block_id, cells: self.payload.cell_by_field_id, height: self.payload.height, visibility: self.payload.visibility, diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 3b48e313a9..a53314a812 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -1,5 +1,6 @@ use crate::entities::FieldType; use crate::services::field::*; +use crate::services::row::RowRevisionBuilder; use flowy_grid_data_model::revision::BuildGridContext; use flowy_sync::client_grid::GridBuilder; @@ -30,3 +31,47 @@ pub fn make_default_grid() -> BuildGridContext { grid_builder.add_empty_row(); grid_builder.build() } + +pub fn make_default_board() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); + // text + let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Name") + .visibility(true) + .primary(true) + .build(); + grid_builder.add_field(text_field); + + // single select + let in_progress_option = SelectOptionPB::new("In progress"); + let not_started_option = SelectOptionPB::new("Not started"); + let done_option = SelectOptionPB::new("Done"); + let single_select = SingleSelectTypeOptionBuilder::default() + .add_option(not_started_option.clone()) + .add_option(in_progress_option.clone()) + .add_option(done_option.clone()); + let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); + let single_select_field_id = single_select_field.id.clone(); + grid_builder.add_field(single_select_field); + + let field_revs = grid_builder.field_revs(); + let block_id = grid_builder.block_id(); + + // rows + let row_1 = RowRevisionBuilder::new(block_id, field_revs) + .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) + .build(); + grid_builder.add_row(row_1); + + let row_2 = RowRevisionBuilder::new(block_id, field_revs) + .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) + .build(); + grid_builder.add_row(row_2); + + let row_3 = RowRevisionBuilder::new(block_id, field_revs) + .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) + .build(); + grid_builder.add_row(row_3); + + grid_builder.build() +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index 216a4d4acd..d2a430f591 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -18,7 +18,7 @@ pub struct GridRowTestBuilder<'a> { impl<'a> GridRowTestBuilder<'a> { pub fn new(block_id: &str, field_revs: &'a [Arc]) -> Self { assert_eq!(field_revs.len(), FieldType::COUNT); - let inner_builder = RowRevisionBuilder::new(field_revs); + let inner_builder = RowRevisionBuilder::new(block_id, field_revs); Self { block_id: block_id.to_owned(), field_revs, @@ -77,8 +77,7 @@ impl<'a> GridRowTestBuilder<'a> { let type_option = SingleSelectTypeOptionPB::from(&single_select_field); let option = f(type_option.options); self.inner_builder - .insert_select_option_cell(&single_select_field.id, option.id) - .unwrap(); + .insert_select_option_cell(&single_select_field.id, option.id); single_select_field.id.clone() } @@ -96,8 +95,7 @@ impl<'a> GridRowTestBuilder<'a> { .collect::>() .join(SELECTION_IDS_SEPARATOR); self.inner_builder - .insert_select_option_cell(&multi_select_field.id, ops_ids) - .unwrap(); + .insert_select_option_cell(&multi_select_field.id, ops_ids); multi_select_field.id.clone() } @@ -115,7 +113,7 @@ impl<'a> GridRowTestBuilder<'a> { } pub fn build(self) -> RowRevision { - self.inner_builder.build(&self.block_id) + self.inner_builder.build() } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs index 01424cef74..9987b60671 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs @@ -39,8 +39,8 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { let single_select = SingleSelectTypeOptionBuilder::default() - .option(SelectOptionPB::new("Done")) - .option(SelectOptionPB::new("Progress")); + .add_option(SelectOptionPB::new("Done")) + .add_option(SelectOptionPB::new("Progress")); let field_rev = FieldBuilder::new(single_select).name("Name").visibility(true).build(); let cloned_field_rev = field_rev.clone(); diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index f924b3e803..09f52ddb15 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -138,9 +138,9 @@ fn make_test_grid() -> BuildGridContext { FieldType::SingleSelect => { // Single Select let single_select = SingleSelectTypeOptionBuilder::default() - .option(SelectOptionPB::new(COMPLETED)) - .option(SelectOptionPB::new(PLANNED)) - .option(SelectOptionPB::new(PAUSED)); + .add_option(SelectOptionPB::new(COMPLETED)) + .add_option(SelectOptionPB::new(PLANNED)) + .add_option(SelectOptionPB::new(PAUSED)); let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); grid_builder.add_field(single_select_field); } diff --git a/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs index 24bb71d89c..cb1d90409e 100644 --- a/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs @@ -1,6 +1,6 @@ use bytes::Bytes; use flowy_database::ConnectionPool; -use flowy_folder::entities::ViewDataType; +use flowy_folder::entities::{SubViewDataTypePB, ViewDataTypePB}; use flowy_folder::manager::{ViewDataProcessor, ViewDataProcessorMap}; use flowy_folder::{ errors::{internal_error, FlowyError}, @@ -8,7 +8,7 @@ use flowy_folder::{ manager::FolderManager, }; use flowy_grid::manager::{make_grid_view_data, GridManager}; -use flowy_grid::util::make_default_grid; +use flowy_grid::util::{make_default_board, make_default_grid}; use flowy_grid_data_model::revision::BuildGridContext; use flowy_net::ClientServerConfiguration; use flowy_net::{ @@ -66,7 +66,7 @@ fn make_view_data_processor( text_block_manager: Arc, grid_manager: Arc, ) -> ViewDataProcessorMap { - let mut map: HashMap> = HashMap::new(); + let mut map: HashMap> = HashMap::new(); let block_data_impl = TextBlockViewDataProcessor(text_block_manager); map.insert(block_data_impl.data_type(), Arc::new(block_data_impl)); @@ -180,7 +180,12 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { }) } - fn create_default_view(&self, user_id: &str, view_id: &str) -> FutureResult { + fn create_default_view( + &self, + user_id: &str, + view_id: &str, + _sub_data_type: Option, + ) -> FutureResult { let user_id = user_id.to_string(); let view_id = view_id.to_string(); let manager = self.0.clone(); @@ -203,8 +208,8 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { FutureResult::new(async move { Ok(Bytes::from(data)) }) } - fn data_type(&self) -> ViewDataType { - ViewDataType::TextBlock + fn data_type(&self) -> ViewDataTypePB { + ViewDataTypePB::TextBlock } } @@ -252,8 +257,16 @@ impl ViewDataProcessor for GridViewDataProcessor { }) } - fn create_default_view(&self, user_id: &str, view_id: &str) -> FutureResult { - let build_context = make_default_grid(); + fn create_default_view( + &self, + user_id: &str, + view_id: &str, + sub_data_type: Option, + ) -> FutureResult { + let build_context = match sub_data_type.unwrap() { + SubViewDataTypePB::Grid => make_default_grid(), + SubViewDataTypePB::Board => make_default_board(), + }; let user_id = user_id.to_string(); let view_id = view_id.to_string(); let grid_manager = self.0.clone(); @@ -278,7 +291,7 @@ impl ViewDataProcessor for GridViewDataProcessor { }) } - fn data_type(&self) -> ViewDataType { - ViewDataType::Grid + fn data_type(&self) -> ViewDataTypePB { + ViewDataTypePB::Database } } diff --git a/frontend/rust-lib/flowy-test/src/helper.rs b/frontend/rust-lib/flowy-test/src/helper.rs index 1265bd695c..6254955ceb 100644 --- a/frontend/rust-lib/flowy-test/src/helper.rs +++ b/frontend/rust-lib/flowy-test/src/helper.rs @@ -25,7 +25,7 @@ pub struct ViewTest { impl ViewTest { #[allow(dead_code)] - pub async fn new(sdk: &FlowySDKTest, data_type: ViewDataType, data: Vec) -> Self { + pub async fn new(sdk: &FlowySDKTest, data_type: ViewDataTypePB, data: Vec) -> Self { let workspace = create_workspace(sdk, "Workspace", "").await; open_workspace(sdk, &workspace.id).await; let app = create_app(sdk, "App", "AppFlowy GitHub Project", &workspace.id).await; @@ -39,11 +39,11 @@ impl ViewTest { } pub async fn new_grid_view(sdk: &FlowySDKTest, data: Vec) -> Self { - Self::new(sdk, ViewDataType::Grid, data).await + Self::new(sdk, ViewDataTypePB::Database, data).await } pub async fn new_text_block_view(sdk: &FlowySDKTest) -> Self { - Self::new(sdk, ViewDataType::TextBlock, vec![]).await + Self::new(sdk, ViewDataTypePB::TextBlock, vec![]).await } } @@ -90,7 +90,7 @@ async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &s app } -async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataType, data: Vec) -> ViewPB { +async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataTypePB, data: Vec) -> ViewPB { let request = CreateViewPayloadPB { belong_to_id: app_id.to_string(), name: "View A".to_string(), diff --git a/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs b/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs index 7a81bbb191..e542fb9bbd 100644 --- a/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs +++ b/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs @@ -53,7 +53,7 @@ impl std::convert::From for TrashRevision { #[repr(u8)] pub enum ViewDataTypeRevision { TextBlock = 0, - Grid = 1, + Database = 1, } impl std::default::Default for ViewDataTypeRevision { From b5ecd7dedcd167e7c1506edbeab0cb15503c5d84 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 17:58:01 +0800 Subject: [PATCH 054/224] test: next runes --- .../lib/src/document/text_delta.dart | 4 +-- .../flowy_editor/test/delta_test.dart | 34 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index b1e6525c49..6cbc015569 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -477,7 +477,7 @@ class Delta extends Iterable { int prevRunePosition(int pos) { if (pos == 0) { - return pos; + return pos - 1; } _rawString ??= _operations.whereType().map((op) => op.content).join(); @@ -498,7 +498,7 @@ class Delta extends Iterable { } } - return pos; + return stringContent.length; } String toRawString() { diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 2b0d529414..3114de8dd7 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -279,9 +279,35 @@ void main() { expect(delta, expected); }); }); - test("stringIndexes", () { - final indexes = stringIndexes('😊'); - expect(indexes[0], 0); - expect(indexes[1], 0); + group('runes', () { + test("stringIndexes", () { + final indexes = stringIndexes('😊'); + expect(indexes[0], 0); + expect(indexes[1], 0); + }); + test("next rune 1", () { + final delta = Delta()..insert('😊'); + expect(delta.nextRunePosition(0), 2); + }); + test("next rune 2", () { + final delta = Delta()..insert('😊a'); + expect(delta.nextRunePosition(0), 2); + }); + test("next rune 3", () { + final delta = Delta()..insert('😊陈'); + expect(delta.nextRunePosition(2), 3); + }); + test("prev rune 1", () { + final delta = Delta()..insert('😊陈'); + expect(delta.prevRunePosition(2), 0); + }); + test("prev rune 2", () { + final delta = Delta()..insert('😊'); + expect(delta.prevRunePosition(2), 0); + }); + test("prev rune 3", () { + final delta = Delta()..insert('😊'); + expect(delta.prevRunePosition(0), -1); + }); }); } From 223c8cafd475c45f0bf3c7ed010b8e5dde32943d Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Wed, 10 Aug 2022 18:07:25 +0800 Subject: [PATCH 055/224] fix: crashes when deleting emoji --- .../internal_key_event_handlers/delete_text_handler.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 1bb6872ff4..5e2fdfd453 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -25,7 +25,7 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { TransactionBuilder transactionBuilder = TransactionBuilder(editorState); if (textNodes.length == 1) { final textNode = textNodes.first; - final index = selection.start.offset - 1; + final index = textNode.delta.prevRunePosition(selection.start.offset); if (index < 0) { // 1. style if (textNode.subtype != null) { @@ -62,8 +62,8 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { if (selection.isCollapsed) { transactionBuilder.deleteText( textNode, - selection.start.offset - 1, - 1, + index, + selection.start.offset - index, ); } else { transactionBuilder.deleteText( From 32d5edff81d153c1af78b0466e7fb7e5acd231f2 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 19:15:36 +0800 Subject: [PATCH 056/224] refactor: refactor class SingleSelectTypeOptionContext --- .../plugins/board/application/field_group.dart | 0 .../application/field/type_option/date_bloc.dart | 2 -- .../type_option/multi_select_type_option.dart | 3 +-- .../field/type_option/number_bloc.dart | 2 -- .../select_option_type_option_bloc.dart | 4 ++-- .../type_option/single_select_type_option.dart | 4 +--- .../type_option/type_option_data_controller.dart | 15 +++++++++++++++ .../widgets/header/type_option/rich_text.dart | 2 -- .../widgets/header/type_option/select_option.dart | 6 ++++-- .../widgets/header/type_option/url.dart | 2 -- 10 files changed, 23 insertions(+), 17 deletions(-) delete mode 100644 frontend/app_flowy/lib/plugins/board/application/field_group.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/field_group.dart b/frontend/app_flowy/lib/plugins/board/application/field_group.dart deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart index 15c052060d..1b9c3841ff 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart @@ -8,8 +8,6 @@ import 'package:protobuf/protobuf.dart'; import 'type_option_data_controller.dart'; part 'date_bloc.freezed.dart'; -typedef DateTypeOptionContext = TypeOptionContext; - class DateTypeOptionDataParser extends TypeOptionDataParser { @override DateTypeOption fromBuffer(List buffer) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart index a05e5ea2ff..acf46a0b37 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart @@ -8,8 +8,7 @@ import 'type_option_service.dart'; import 'package:protobuf/protobuf.dart'; class MultiSelectTypeOptionContext - extends TypeOptionContext - with SelectOptionTypeOptionAction { + extends TypeOptionContext with ISelectOptionAction { final TypeOptionFFIService service; MultiSelectTypeOptionContext({ diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart index fc3d8b0822..7b723324bf 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart @@ -8,8 +8,6 @@ import 'type_option_data_controller.dart'; part 'number_bloc.freezed.dart'; -typedef NumberTypeOptionContext = TypeOptionContext; - class NumberTypeOptionWidgetDataParser extends TypeOptionDataParser { @override diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart index 6e4aca6cc9..ef7fe53de3 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:dartz/dartz.dart'; part 'select_option_type_option_bloc.freezed.dart'; -abstract class SelectOptionTypeOptionAction { +abstract class ISelectOptionAction { Future> Function(String) get insertOption; List Function(SelectOptionPB) get deleteOption; @@ -15,7 +15,7 @@ abstract class SelectOptionTypeOptionAction { class SelectOptionTypeOptionBloc extends Bloc { - final SelectOptionTypeOptionAction typeOptionAction; + final ISelectOptionAction typeOptionAction; SelectOptionTypeOptionBloc({ required List options, diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart index e4de10a3ef..d8f728f1b5 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart @@ -7,9 +7,7 @@ import 'select_option_type_option_bloc.dart'; import 'type_option_data_controller.dart'; import 'type_option_service.dart'; -class SingleSelectTypeOptionContext - extends TypeOptionContext - with SelectOptionTypeOptionAction { +class SingleSelectTypeOptionContext with ISelectOptionAction { final TypeOptionFFIService service; SingleSelectTypeOptionContext({ diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart index 75b373f787..424abc87ce 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart @@ -1,9 +1,15 @@ import 'package:flowy_infra/notifier.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:protobuf/protobuf.dart'; import 'package:flowy_sdk/log.dart'; @@ -11,6 +17,15 @@ abstract class TypeOptionDataParser { T fromBuffer(List buffer); } +typedef NumberTypeOptionContext = TypeOptionContext; +typedef RichTextTypeOptionContext = TypeOptionContext; +typedef CheckboxTypeOptionContext = TypeOptionContext; +typedef URLTypeOptionContext = TypeOptionContext; +typedef DateTypeOptionContext = TypeOptionContext; + +typedef SingleSelectTypeOptionContext + = TypeOptionContext; + class TypeOptionContext { T? _typeOptionObject; final TypeOptionDataParser dataParser; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart index 4ee9e1e6ae..c24f98a28f 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart @@ -3,8 +3,6 @@ import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -typedef RichTextTypeOptionContext = TypeOptionContext; - class RichTextTypeOptionWidgetDataParser extends TypeOptionDataParser { @override diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart index 5d6304ea7a..d36df1fea7 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart @@ -20,7 +20,7 @@ class SelectOptionTypeOptionWidget extends StatelessWidget { final List options; final VoidCallback beginEdit; final TypeOptionOverlayDelegate overlayDelegate; - final SelectOptionTypeOptionAction typeOptionAction; + final ISelectOptionAction typeOptionAction; const SelectOptionTypeOptionWidget({ required this.options, @@ -34,7 +34,9 @@ class SelectOptionTypeOptionWidget extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SelectOptionTypeOptionBloc( - options: options, typeOptionAction: typeOptionAction), + options: options, + typeOptionAction: typeOptionAction, + ), child: BlocBuilder( builder: (context, state) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart index 3521b336cf..07a341493a 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart @@ -3,8 +3,6 @@ import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -typedef URLTypeOptionContext = TypeOptionContext; - class URLTypeOptionWidgetDataParser extends TypeOptionDataParser { @override From 72c5c937ae8c85ee7c1c8016b1530b6625778a9a Mon Sep 17 00:00:00 2001 From: Aryaman Date: Wed, 10 Aug 2022 17:03:01 +0530 Subject: [PATCH 057/224] fix: context menu now opens behind the icon as well --- .../home/menu/app/section/item.dart | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index bfc51f4bf9..cb3146a70c 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -42,18 +42,28 @@ class ViewSectionItem extends StatelessWidget { ], child: BlocBuilder( builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: InkWell( - onTap: () => onSelected(context.read().state.view), - child: FlowyHover( - style: HoverStyle(hoverColor: theme.bg3), - builder: (_, onHover) => - _render(context, onHover, state, theme.iconColor), - setSelected: () => state.isEditing || isSelected, - ), - ), - ); + return ViewDisclosureRegion( + onTap: () => context + .read() + .add(const ViewEvent.setIsEditing(true)), + onSelected: (action) { + context + .read() + .add(const ViewEvent.setIsEditing(false)); + _handleAction(context, action); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: InkWell( + onTap: () => onSelected(context.read().state.view), + child: FlowyHover( + style: HoverStyle(hoverColor: theme.bg3), + builder: (_, onHover) => + _render(context, onHover, state, theme.iconColor), + setSelected: () => state.isEditing || isSelected, + ), + ), + )); }, ), ); @@ -85,20 +95,13 @@ class ViewSectionItem extends StatelessWidget { ); } - return ViewDisclosureRegion( - onTap: () => - context.read().add(const ViewEvent.setIsEditing(true)), - onSelected: (action) { - context.read().add(const ViewEvent.setIsEditing(false)); - _handleAction(context, action); - }, - child: SizedBox( - height: 26, - child: Row(children: children).padding( - left: MenuAppSizes.expandedPadding, - right: MenuAppSizes.headerPadding, - ), - )); + return SizedBox( + height: 26, + child: Row(children: children).padding( + left: MenuAppSizes.expandedPadding, + right: MenuAppSizes.headerPadding, + ), + ); } void _handleAction( From ae0012ba370efa271efd3f5df6a6983b665b29ac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 10 Aug 2022 15:30:19 +0800 Subject: [PATCH 058/224] docs: documentation for selection_service --- .../flowy_editor/lib/flowy_editor.dart | 13 +- .../lib/src/document/selection.dart | 45 ++++--- .../lib/src/service/editor_service.dart | 24 +--- .../default_key_event_handlers.dart | 22 ++++ .../lib/src/service/selection_service.dart | 119 ++++++------------ .../flowy_editor/lib/src/service/service.dart | 3 +- 6 files changed, 103 insertions(+), 123 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index a767c08407..9e75790ea3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -1,15 +1,16 @@ library flowy_editor; -export 'src/document/state_tree.dart'; export 'src/document/node.dart'; export 'src/document/path.dart'; +export 'src/document/position.dart'; +export 'src/document/selection.dart'; +export 'src/document/state_tree.dart'; export 'src/document/text_delta.dart'; -export 'src/render/selection/selectable.dart'; +export 'src/editor_state.dart'; +export 'src/operation/operation.dart'; export 'src/operation/transaction.dart'; export 'src/operation/transaction_builder.dart'; -export 'src/operation/operation.dart'; -export 'src/editor_state.dart'; +export 'src/render/selection/selectable.dart'; export 'src/service/editor_service.dart'; -export 'src/document/selection.dart'; -export 'src/document/position.dart'; export 'src/service/render_plugin_service.dart'; +export 'src/service/service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart index 692cc8c91b..7cf305b131 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart @@ -2,15 +2,26 @@ import 'package:flowy_editor/src/document/path.dart'; import 'package:flowy_editor/src/document/position.dart'; import 'package:flowy_editor/src/extensions/path_extensions.dart'; +/// Selection represents the selected area or the cursor area in the editor. +/// +/// [Selection] is directional. +/// +/// 1. forward,the end position is before the start position. +/// 2. backward, the end position is after the start position. +/// 3. collapsed, the end position is equal to the start position. class Selection { - final Position start; - final Position end; - + /// Create a selection with [start], [end]. Selection({ required this.start, required this.end, }); + /// Create a selection with [Path], [startOffset] and [endOffset]. + /// + /// The [endOffset] is optional. + /// + /// This constructor will return a collapsed [Selection] if [endOffset] is null. + /// Selection.single({ required Path path, required int startOffset, @@ -18,10 +29,21 @@ class Selection { }) : start = Position(path: path, offset: startOffset), end = Position(path: path, offset: endOffset ?? startOffset); + /// Create a collapsed selection with [position]. Selection.collapsed(Position position) : start = position, end = position; + final Position start; + final Position end; + + bool get isCollapsed => start == end; + bool get isSingle => pathEquals(start.path, end.path); + bool get isForward => + start.path >= end.path && !pathEquals(start.path, end.path); + bool get isBackward => + start.path <= end.path && !pathEquals(start.path, end.path); + Selection collapse({bool atStart = false}) { if (atStart) { return Selection(start: start, end: start); @@ -30,13 +52,6 @@ class Selection { } } - bool get isCollapsed => start == end; - bool get isSingle => pathEquals(start.path, end.path); - bool get isUpward => - start.path >= end.path && !pathEquals(start.path, end.path); - bool get isDownward => - start.path <= end.path && !pathEquals(start.path, end.path); - Selection copyWith({Position? start, Position? end}) { return Selection( start: start ?? this.start, @@ -46,13 +61,10 @@ class Selection { Selection copy() => Selection(start: start, end: end); - @override - String toString() => '[Selection] start = $start, end = $end'; - Map toJson() { return { - "start": start.toJson(), - "end": end.toJson(), + 'start': start.toJson(), + 'end': end.toJson(), }; } @@ -69,4 +81,7 @@ class Selection { @override int get hashCode => Object.hash(start, end); + + @override + String toString() => '[Selection] start = $start, end = $end'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart index b596f83c2a..b2d649d246 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/editor_service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/src/editor_state.dart'; @@ -9,15 +10,6 @@ import 'package:flowy_editor/src/render/rich_text/number_list_text.dart'; import 'package:flowy_editor/src/render/rich_text/quoted_text.dart'; import 'package:flowy_editor/src/render/rich_text/rich_text.dart'; import 'package:flowy_editor/src/service/input_service.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flowy_editor/src/service/scroll_service.dart'; @@ -34,18 +26,6 @@ NodeWidgetBuilders defaultBuilders = { 'text/quote': QuotedTextNodeWidgetBuilder(), }; -List defaultKeyEventHandler = [ - deleteTextHandler, - slashShortcutHandler, - flowyDeleteNodesHandler, - arrowKeysHandler, - copyPasteKeysHandler, - redoUndoKeysHandler, - enterWithoutShiftInTextNodesHandler, - updateTextStyleByCommandXHandler, - whiteSpaceHandler, -]; - class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, @@ -98,7 +78,7 @@ class _FlowyEditorState extends State { child: FlowyKeyboard( key: editorState.service.keyboardServiceKey, handlers: [ - ...defaultKeyEventHandler, + ...defaultKeyEventHandlers, ...widget.keyEventHandlers, ], editorState: editorState, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart new file mode 100644 index 0000000000..d03211461c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -0,0 +1,22 @@ +import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:flowy_editor/src/service/keyboard_service.dart'; + +List defaultKeyEventHandlers = [ + deleteTextHandler, + slashShortcutHandler, + flowyDeleteNodesHandler, + arrowKeysHandler, + copyPasteKeysHandler, + redoUndoKeysHandler, + enterWithoutShiftInTextNodesHandler, + updateTextStyleByCommandXHandler, + whiteSpaceHandler, +]; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index 19d355847f..368bac9c92 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -15,67 +15,60 @@ import 'package:flowy_editor/src/render/selection/cursor_widget.dart'; import 'package:flowy_editor/src/render/selection/selectable.dart'; import 'package:flowy_editor/src/render/selection/selection_widget.dart'; -/// Process selection and cursor +/// [FlowySelectionService] is responsible for processing +/// the [Selection] changes and updates. +/// +/// Usually, this service can be obtained by the following code. +/// ```dart +/// final selectionService = editorState.service.selectionService; +/// +/// /** get current selection value*/ +/// final selection = selectionService.currentSelection.value; +/// +/// /** get current selected nodes*/ +/// final nodes = selectionService.currentSelectedNodes; +/// ``` +/// mixin FlowySelectionService on State { - /// Returns the current [Selection] + /// The current [Selection] in editor. + /// + /// The value is null if there is no nodes are selected. ValueNotifier get currentSelection; - /// Returns the current selected [Node]s. + /// The current selected [Node]s in editor. /// - /// The order of the return is determined according to the selected order. + /// The order of the result is determined according to the [currentSelection]. + /// The result are ordered from back to front if the selection is forward. + /// The result are ordered from front to back if the selection is backward. + /// + /// For example, Here is an array of selected nodes, [n1, n2, n3]. + /// The result will be [n3, n2, n1] if the selection is forward, + /// and [n1, n2, n3] if the selection is backward. + /// + /// Returns empty result if there is no nodes are selected. List get currentSelectedNodes; - /// Update the selection or cursor. + /// Updates the selection. /// - /// If selection is collapsed, this method will - /// update the position of the cursor. - /// Otherwise, will update the selection. + /// The editor will update selection area and popup list area + /// if the [selection] is not collapsed, + /// otherwise, will update the cursor area. void updateSelection(Selection selection); - /// Clear the selection or cursor. + /// Clears the selection area, cursor area and the popup list area. void clearSelection(); - /// ------------------ Selection ------------------------ - - List rects(); - - Position? hitTest(Offset? offset); - - /// + /// Returns the [Node]s in [Selection]. List getNodesInSelection(Selection selection); - /// ------------------ Selection ------------------------ - - /// ------------------ Offset ------------------------ - - /// Return the [Node] or [Null] in single selection. + /// Returns the [Node] containing to the offset. /// - /// [offset] is under the global coordinate system. + /// [offset] must be under the global coordinate system. Node? getNodeInOffset(Offset offset); - /// Returns selected [Node]s. Empty list would be returned - /// if no nodes are in range. - /// - /// - /// [start] and [end] are under the global coordinate system. - /// - List getNodeInRange(Offset start, Offset end); - - /// Return [bool] to identify the [Node] is in Range or not. - /// - /// [start] and [end] are under the global coordinate system. - bool isNodeInRange( - Node node, - Offset start, - Offset end, - ); - - /// Return [bool] to identify the [Node] contains [Offset] or not. - /// - /// [offset] is under the global coordinate system. - bool isNodeInOffset(Node node, Offset offset); - - /// ------------------ Offset ------------------------ + // TODO: need to be documented. + List rects(); + Position? hitTest(Offset? offset); } class FlowySelection extends StatefulWidget { @@ -207,36 +200,6 @@ class _FlowySelectionState extends State return _lowerBoundInDocument(offset); } - @override - List getNodeInRange(Offset start, Offset end) { - final startNode = _lowerBoundInDocument(start); - final endNode = _upperBoundInDocument(end); - return NodeIterator(editorState.document, startNode, endNode).toList(); - } - - @override - bool isNodeInOffset(Node node, Offset offset) { - final renderBox = node.renderBox; - if (renderBox != null) { - final boxOffset = renderBox.localToGlobal(Offset.zero); - final boxRect = boxOffset & renderBox.size; - return boxRect.contains(offset); - } - return false; - } - - @override - bool isNodeInRange(Node node, Offset start, Offset end) { - final renderBox = node.renderBox; - if (renderBox != null) { - final rect = Rect.fromPoints(start, end); - final boxOffset = renderBox.localToGlobal(Offset.zero); - final boxRect = boxOffset & renderBox.size; - return rect.overlaps(boxRect); - } - return false; - } - void _onDoubleTapDown(TapDownDetails details) { final offset = details.globalPosition; final node = getNodeInOffset(offset); @@ -395,13 +358,13 @@ class _FlowySelectionState extends State // text: ghijkl // text: mn>opqr if (index == 0) { - if (selection.isDownward) { + if (selection.isBackward) { newSelection = selection.copyWith(end: selectable.end()); } else { newSelection = selection.copyWith(start: selectable.start()); } } else if (index == nodes.length - 1) { - if (selection.isDownward) { + if (selection.isBackward) { newSelection = selection.copyWith(start: selectable.start()); } else { newSelection = selection.copyWith(end: selectable.end()); @@ -498,7 +461,7 @@ class _FlowySelectionState extends State /// TODO: It is necessary to calculate the relative speed /// according to the gap and move forward more gently. - final distance = 10.0; + const distance = 10.0; if (offset.dy <= topLimit && !isDownward) { // up editorState.service.scrollService?.scrollTo(dy - distance); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart index cdf137bee8..fc2e4e3f31 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flowy_editor/src/service/render_plugin_service.dart'; import 'package:flowy_editor/src/service/scroll_service.dart'; import 'package:flowy_editor/src/service/selection_service.dart'; import 'package:flowy_editor/src/service/toolbar_service.dart'; +import 'package:flutter/material.dart'; class FlowyService { // selection service From 1eec97c761e75774123f6f995e3c44f2d89fe1ce Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 10 Aug 2022 15:56:52 +0800 Subject: [PATCH 059/224] chore: refactor the service type --- .../app_flowy/packages/flowy_editor/lib/flowy_editor.dart | 4 ++++ .../packages/flowy_editor/lib/src/service/input_service.dart | 5 ++--- .../flowy_editor/lib/src/service/keyboard_service.dart | 4 ++-- .../flowy_editor/lib/src/service/scroll_service.dart | 5 +++-- .../flowy_editor/lib/src/service/selection_service.dart | 5 +++-- .../flowy_editor/lib/src/service/toolbar_service.dart | 5 +++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 9e75790ea3..418b4d7ce0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -14,3 +14,7 @@ export 'src/render/selection/selectable.dart'; export 'src/service/editor_service.dart'; export 'src/service/render_plugin_service.dart'; export 'src/service/service.dart'; +export 'src/service/selection_service.dart'; +export 'src/service/scroll_service.dart'; +export 'src/service/keyboard_service.dart'; +export 'src/service/input_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart index c52a411905..da755ab346 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart @@ -7,7 +7,7 @@ import 'package:flowy_editor/src/editor_state.dart'; import 'package:flowy_editor/src/extensions/node_extensions.dart'; import 'package:flowy_editor/src/operation/transaction_builder.dart'; -mixin FlowyInputService { +abstract class FlowyInputService { void attach(TextEditingValue textEditingValue); void apply(List deltas); void close(); @@ -29,8 +29,7 @@ class FlowyInput extends StatefulWidget { } class _FlowyInputState extends State - with FlowyInputService - implements DeltaTextInputClient { + implements FlowyInputService, DeltaTextInputClient { TextInputConnection? _textInputConnection; TextRange? _composingTextRange; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart index 81ce8348a4..ef8165049b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; -mixin FlowyKeyboardService on State { +abstract class FlowyKeyboardService { void enable(); void disable(); } @@ -31,7 +31,7 @@ class FlowyKeyboard extends StatefulWidget { } class _FlowyKeyboardState extends State - with FlowyKeyboardService { + implements FlowyKeyboardService { final FocusNode _focusNode = FocusNode(debugLabel: 'flowy_keyboard_service'); bool isFocus = true; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart index af48a78c49..5201a18942 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart @@ -1,7 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -mixin FlowyScrollService on State { +abstract class FlowyScrollService { double get dy; void scrollTo(double dy); @@ -22,7 +22,8 @@ class FlowyScroll extends StatefulWidget { State createState() => _FlowyScrollState(); } -class _FlowyScrollState extends State with FlowyScrollService { +class _FlowyScrollState extends State + implements FlowyScrollService { final _scrollController = ScrollController(); final _scrollViewKey = GlobalKey(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index 368bac9c92..56bcb07cd1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -29,7 +29,7 @@ import 'package:flowy_editor/src/render/selection/selection_widget.dart'; /// final nodes = selectionService.currentSelectedNodes; /// ``` /// -mixin FlowySelectionService on State { +abstract class FlowySelectionService { /// The current [Selection] in editor. /// /// The value is null if there is no nodes are selected. @@ -90,7 +90,8 @@ class FlowySelection extends StatefulWidget { } class _FlowySelectionState extends State - with FlowySelectionService, WidgetsBindingObserver { + with WidgetsBindingObserver + implements FlowySelectionService { final _cursorKey = GlobalKey(debugLabel: 'cursor'); final List _selectionOverlays = []; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart index a45fe8b778..aaf52bc20c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/src/render/selection/toolbar_widget.dart'; -mixin FlowyToolbarService { +abstract class FlowyToolbarService { /// Show the toolbar widget beside the offset. void showInOffset(Offset offset, LayerLink layerLink); @@ -25,7 +25,8 @@ class FlowyToolbar extends StatefulWidget { State createState() => _FlowyToolbarState(); } -class _FlowyToolbarState extends State with FlowyToolbarService { +class _FlowyToolbarState extends State + implements FlowyToolbarService { OverlayEntry? _toolbarOverlay; @override From 50f8e1f5d021e9444727783da0be1517ac3216d8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 10 Aug 2022 20:04:06 +0800 Subject: [PATCH 060/224] feat: implement automatically wrap when the selection changes --- .../lib/src/document/selection.dart | 6 +- .../src/render/rich_text/flowy_rich_text.dart | 2 +- .../arrow_keys_handler.dart | 9 +- .../service/selection/selection_gesture.dart | 113 +++ .../lib/src/service/selection_service.dart | 713 ++++++++---------- 5 files changed, 417 insertions(+), 426 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/src/service/selection/selection_gesture.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart index 7cf305b131..68aecba8fc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart @@ -40,9 +40,11 @@ class Selection { bool get isCollapsed => start == end; bool get isSingle => pathEquals(start.path, end.path); bool get isForward => - start.path >= end.path && !pathEquals(start.path, end.path); + (start.path >= end.path && !pathEquals(start.path, end.path)) || + (isSingle && start.offset > end.offset); bool get isBackward => - start.path <= end.path && !pathEquals(start.path, end.path); + (start.path <= end.path && !pathEquals(start.path, end.path)) || + (isSingle && start.offset < end.offset); Selection collapse({bool atStart = false}) { if (atStart) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 350a5c71e6..fb0009ad73 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -59,7 +59,7 @@ class _FlowyRichTextState extends State with Selectable { @override Position end() => Position( - path: widget.textNode.path, offset: widget.textNode.toRawString().length); + path: widget.textNode.path, offset: widget.textNode.delta.length); @override Rect? getCursorRectInPosition(Position position) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index 83243d2dc7..d4ddd8436c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,5 +1,4 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -41,25 +40,25 @@ extension on Position { } Position? _goUp(EditorState editorState) { - final rects = editorState.service.selectionService.rects(); + final rects = editorState.service.selectionService.selectionRects; if (rects.isEmpty) { return null; } final first = rects.first; final firstOffset = Offset(first.left, first.top); final hitOffset = firstOffset - Offset(0, first.height * 0.5); - return editorState.service.selectionService.hitTest(hitOffset); + return editorState.service.selectionService.getPositionInOffset(hitOffset); } Position? _goDown(EditorState editorState) { - final rects = editorState.service.selectionService.rects(); + final rects = editorState.service.selectionService.selectionRects; if (rects.isEmpty) { return null; } final first = rects.last; final firstOffset = Offset(first.right, first.bottom); final hitOffset = firstOffset + Offset(0, first.height * 0.5); - return editorState.service.selectionService.hitTest(hitOffset); + return editorState.service.selectionService.getPositionInOffset(hitOffset); } KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection/selection_gesture.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection/selection_gesture.dart new file mode 100644 index 0000000000..11a6326d26 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection/selection_gesture.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] +/// for a while. So we need to implement our own GestureDetector. +@immutable +class SelectionGestureDetector extends StatefulWidget { + const SelectionGestureDetector({ + Key? key, + this.child, + this.onTapDown, + this.onDoubleTapDown, + this.onTripleTapDown, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd, + }) : super(key: key); + + @override + State createState() => + SelectionGestureDetectorState(); + + final Widget? child; + + final GestureTapDownCallback? onTapDown; + final GestureTapDownCallback? onDoubleTapDown; + final GestureTapDownCallback? onTripleTapDown; + final GestureDragStartCallback? onPanStart; + final GestureDragUpdateCallback? onPanUpdate; + final GestureDragEndCallback? onPanEnd; +} + +class SelectionGestureDetectorState extends State { + bool _isDoubleTap = false; + Timer? _doubleTapTimer; + int _tripleTabCount = 0; + Timer? _tripleTabTimer; + + final kTripleTapTimeout = const Duration(milliseconds: 500); + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(), + (recognizer) { + recognizer + ..onStart = widget.onPanStart + ..onUpdate = widget.onPanUpdate + ..onEnd = widget.onPanEnd; + }, + ), + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recognizer) { + recognizer.onTapDown = _tapDownDelegate; + }, + ), + }, + child: widget.child, + ); + } + + _tapDownDelegate(TapDownDetails tapDownDetails) { + if (_tripleTabCount == 2) { + _tripleTabCount = 0; + _tripleTabTimer?.cancel(); + _tripleTabTimer = null; + if (widget.onTripleTapDown != null) { + widget.onTripleTapDown!(tapDownDetails); + } + } else if (_isDoubleTap) { + _isDoubleTap = false; + _doubleTapTimer?.cancel(); + _doubleTapTimer = null; + if (widget.onDoubleTapDown != null) { + widget.onDoubleTapDown!(tapDownDetails); + } + _tripleTabCount++; + } else { + if (widget.onTapDown != null) { + widget.onTapDown!(tapDownDetails); + } + + _isDoubleTap = true; + _doubleTapTimer?.cancel(); + _doubleTapTimer = Timer(kDoubleTapTimeout, () { + _isDoubleTap = false; + _doubleTapTimer = null; + }); + + _tripleTabCount = 1; + _tripleTabTimer?.cancel(); + _tripleTabTimer = Timer(kTripleTapTimeout, () { + _tripleTabCount = 0; + _tripleTabTimer = null; + }); + } + } + + @override + void dispose() { + _doubleTapTimer?.cancel(); + _tripleTabTimer?.cancel(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index 56bcb07cd1..4a4fd6002c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -1,19 +1,18 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/document/node_iterator.dart'; import 'package:flowy_editor/src/document/position.dart'; import 'package:flowy_editor/src/document/selection.dart'; -import 'package:flowy_editor/src/document/state_tree.dart'; import 'package:flowy_editor/src/editor_state.dart'; import 'package:flowy_editor/src/extensions/node_extensions.dart'; +import 'package:flowy_editor/src/extensions/object_extensions.dart'; +import 'package:flowy_editor/src/extensions/path_extensions.dart'; import 'package:flowy_editor/src/render/selection/cursor_widget.dart'; import 'package:flowy_editor/src/render/selection/selectable.dart'; import 'package:flowy_editor/src/render/selection/selection_widget.dart'; +import 'package:flowy_editor/src/service/selection/selection_gesture.dart'; /// [FlowySelectionService] is responsible for processing /// the [Selection] changes and updates. @@ -50,7 +49,7 @@ abstract class FlowySelectionService { /// Updates the selection. /// - /// The editor will update selection area and popup list area + /// The editor will update selection area and toolbar area /// if the [selection] is not collapsed, /// otherwise, will update the cursor area. void updateSelection(Selection selection); @@ -61,14 +60,20 @@ abstract class FlowySelectionService { /// Returns the [Node]s in [Selection]. List getNodesInSelection(Selection selection); - /// Returns the [Node] containing to the offset. + /// Returns the [Node] containing to the [offset]. /// /// [offset] must be under the global coordinate system. Node? getNodeInOffset(Offset offset); - // TODO: need to be documented. - List rects(); - Position? hitTest(Offset? offset); + /// Returns the [Position] closest to the [offset]. + /// + /// Returns null if there is no nodes are selected. + /// + /// [offset] must be under the global coordinate system. + Position? getPositionInOffset(Offset offset); + + /// The current selection areas's rect in editor. + List get selectionRects; } class FlowySelection extends StatefulWidget { @@ -94,38 +99,25 @@ class _FlowySelectionState extends State implements FlowySelectionService { final _cursorKey = GlobalKey(debugLabel: 'cursor'); - final List _selectionOverlays = []; - final List _cursorOverlays = []; + @override + final List selectionRects = []; + final List _selectionAreas = []; + final List _cursorAreas = []; + OverlayEntry? _debugOverlay; - /// [Pan] and [Tap] must be mutually exclusive. /// Pan - Offset? panStartOffset; - double? panStartScrollDy; - Offset? panEndOffset; - - /// Tap - Offset? tapOffset; - - final List _rects = []; + Offset? _panStartOffset; + double? _panStartScrollDy; EditorState get editorState => widget.editorState; - @override - ValueNotifier currentSelection = ValueNotifier(null); - - @override - List currentSelectedNodes = []; - - @override - List getNodesInSelection(Selection selection) => - _selectedNodesInSelection(editorState.document, selection); - @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + currentSelection.addListener(_onSelectionChange); } @override @@ -142,13 +134,14 @@ class _FlowySelectionState extends State void dispose() { clearSelection(); WidgetsBinding.instance.removeObserver(this); + currentSelection.removeListener(_onSelectionChange); super.dispose(); } @override Widget build(BuildContext context) { - return _SelectionGestureDetector( + return SelectionGestureDetector( onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, @@ -160,23 +153,48 @@ class _FlowySelectionState extends State } @override - List rects() { - return _rects; + ValueNotifier currentSelection = ValueNotifier(null); + + @override + List currentSelectedNodes = []; + + @override + List getNodesInSelection(Selection selection) { + final start = + selection.isBackward ? selection.start.path : selection.end.path; + final end = + selection.isBackward ? selection.end.path : selection.start.path; + assert(start <= end); + final startNode = editorState.document.nodeAtPath(start); + final endNode = editorState.document.nodeAtPath(end); + if (startNode != null && endNode != null) { + final nodes = + NodeIterator(editorState.document, startNode, endNode).toList(); + if (selection.isBackward) { + return nodes; + } else { + return nodes.reversed.toList(growable: false); + } + } + return []; } @override void updateSelection(Selection selection) { - _rects.clear(); + selectionRects.clear(); clearSelection(); - // cursor if (selection.isCollapsed) { - debugPrint('Update cursor'); - _updateCursor(selection.start); + /// updates cursor area. + debugPrint('updating cursor'); + _updateCursorAreas(selection.start); } else { - debugPrint('Update selection'); - _updateSelection(selection); + // updates selection area. + debugPrint('updating selection'); + _updateSelectionAreas(selection); } + + currentSelection.value = selection; } @override @@ -184,194 +202,172 @@ class _FlowySelectionState extends State currentSelectedNodes = []; currentSelection.value = null; - // clear selection - _selectionOverlays + // clear selection areas + _selectionAreas ..forEach((overlay) => overlay.remove()) ..clear(); - // clear cursors - _cursorOverlays + // clear cursor areas + _cursorAreas ..forEach((overlay) => overlay.remove()) ..clear(); - // clear toolbar + // hide toolbar editorState.service.toolbarService?.hide(); } @override Node? getNodeInOffset(Offset offset) { - return _lowerBoundInDocument(offset); - } - - void _onDoubleTapDown(TapDownDetails details) { - final offset = details.globalPosition; - final node = getNodeInOffset(offset); - if (node == null) { - editorState.updateCursorSelection(null); - return; - } - final selectable = node.selectable; - if (selectable == null) { - editorState.updateCursorSelection(null); - return; - } - editorState - .updateCursorSelection(selectable.getWorldBoundaryInOffset(offset)); - } - - void _onTripleTapDown(TapDownDetails details) { - final offset = details.globalPosition; - final node = getNodeInOffset(offset); - if (node == null) { - editorState.updateCursorSelection(null); - return; - } - Selection selection; - if (node is TextNode) { - final textLen = node.delta.length; - selection = Selection( - start: Position(path: node.path, offset: 0), - end: Position(path: node.path, offset: textLen)); - } else { - selection = Selection.collapsed(Position(path: node.path, offset: 0)); - } - editorState.updateCursorSelection(selection); - } - - void _onTapDown(TapDownDetails details) { - // clear old state. - panStartOffset = null; - panEndOffset = null; - - tapOffset = details.globalPosition; - - final position = hitTest(tapOffset); - if (position == null) { - return; - } - final selection = Selection.collapsed(position); - editorState.updateCursorSelection(selection); - - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); + final sortedNodes = + editorState.document.root.children.toList(growable: false); + return _getNodeInOffset( + sortedNodes, + offset, + 0, + sortedNodes.length - 1, + ); } @override - Position? hitTest(Offset? offset) { - if (offset == null) { - editorState.updateCursorSelection(null); - return null; - } + Position? getPositionInOffset(Offset offset) { final node = getNodeInOffset(offset); - if (node == null) { - editorState.updateCursorSelection(null); - return null; - } - final selectable = node.selectable; + final selectable = node?.selectable; if (selectable == null) { - editorState.updateCursorSelection(null); + clearSelection(); return null; } return selectable.getPositionInOffset(offset); } - void _onPanStart(DragStartDetails details) { + void _onTapDown(TapDownDetails details) { // clear old state. - panEndOffset = null; - tapOffset = null; + _panStartOffset = null; + + final position = getPositionInOffset(details.globalPosition); + if (position == null) { + return; + } + final selection = Selection.collapsed(position); + updateSelection(selection); + + _enableInteraction(); + + _showDebugLayerIfNeeded(offset: details.globalPosition); + } + + void _onDoubleTapDown(TapDownDetails details) { + final offset = details.globalPosition; + final node = getNodeInOffset(offset); + final selection = node?.selectable?.getWorldBoundaryInOffset(offset); + if (selection == null) { + clearSelection(); + return; + } + updateSelection(selection); + + _enableInteraction(); + } + + void _onTripleTapDown(TapDownDetails details) { + final offset = details.globalPosition; + final node = getNodeInOffset(offset); + final selectable = node?.selectable; + if (selectable == null) { + clearSelection(); + return; + } + Selection selection = Selection( + start: selectable.start(), + end: selectable.end(), + ); + updateSelection(selection); + + _enableInteraction(); + } + + void _onPanStart(DragStartDetails details) { clearSelection(); - panStartOffset = details.globalPosition; - panStartScrollDy = editorState.service.scrollService?.dy; + _panStartOffset = details.globalPosition; + _panStartScrollDy = editorState.service.scrollService?.dy; - debugPrint('[_onPanStart] panStartOffset = $panStartOffset'); + _enableInteraction(); } void _onPanUpdate(DragUpdateDetails details) { - if (panStartOffset == null || panStartScrollDy == null) { + if (_panStartOffset == null || _panStartScrollDy == null) { return; } - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); + _enableInteraction(); - panEndOffset = details.globalPosition; + final panEndOffset = details.globalPosition; final dy = editorState.service.scrollService?.dy; - var panStartOffsetWithScrollDyGap = panStartOffset!; - if (dy != null) { - panStartOffsetWithScrollDyGap = - panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy); - } + final panStartOffset = dy == null + ? _panStartOffset! + : _panStartOffset!.translate(0, _panStartScrollDy! - dy); - final first = - _lowerBoundInDocument(panStartOffsetWithScrollDyGap).selectable; - final last = _upperBoundInDocument(panEndOffset!).selectable; + final first = getNodeInOffset(panStartOffset)?.selectable; + final last = getNodeInOffset(panEndOffset)?.selectable; // compute the selection in range. if (first != null && last != null) { - bool isDownward; - if (first == last) { - isDownward = panStartOffsetWithScrollDyGap.dx < panEndOffset!.dx; - } else { - isDownward = panStartOffsetWithScrollDyGap.dy < panEndOffset!.dy; - } - final start = first - .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!) - .start; - final end = last - .getSelectionInRange(panStartOffsetWithScrollDyGap, panEndOffset!) - .end; - final selection = Selection( - start: isDownward ? start : end, end: isDownward ? end : start); + bool isDownward = (identical(first, last)) + ? panStartOffset.dx < panEndOffset.dx + : panStartOffset.dy < panEndOffset.dy; + final start = + first.getSelectionInRange(panStartOffset, panEndOffset).start; + final end = last.getSelectionInRange(panStartOffset, panEndOffset).end; + final selection = Selection(start: start, end: end); debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection'); - editorState.updateCursorSelection(selection); - - _scrollUpOrDownIfNeeded(panEndOffset!, isDownward); + updateSelection(selection); } - _showDebugLayerIfNeeded(); + _showDebugLayerIfNeeded(offset: panEndOffset); } void _onPanEnd(DragEndDetails details) { // do nothing } - void _updateSelection(Selection selection) { - final nodes = _selectedNodesInSelection(editorState.document, selection); + void _updateSelectionAreas(Selection selection) { + final nodes = getNodesInSelection(selection); currentSelectedNodes = nodes; - currentSelection.value = selection; + // TODO: need to be refactored. Rect? topmostRect; LayerLink? layerLink; - var index = 0; - for (final node in nodes) { + final backwardNodes = + selection.isBackward ? nodes : nodes.reversed.toList(growable: false); + final backwardSelection = selection.isBackward + ? selection + : selection.copyWith(start: selection.end, end: selection.start); + assert(backwardSelection.isBackward); + + for (var i = 0; i < backwardNodes.length; i++) { + final node = backwardNodes[i]; final selectable = node.selectable; if (selectable == null) { continue; } - var newSelection = selection.copy(); - // In the case of multiple selections, - // we need to return a new selection for each selected node individually. - if (!selection.isSingle) { - // <> means selected. - // text: abcdopqr - if (index == 0) { - if (selection.isBackward) { - newSelection = selection.copyWith(end: selectable.end()); - } else { - newSelection = selection.copyWith(start: selectable.start()); - } - } else if (index == nodes.length - 1) { - if (selection.isBackward) { - newSelection = selection.copyWith(start: selectable.start()); - } else { - newSelection = selection.copyWith(end: selectable.end()); - } + var newSelection = backwardSelection.copy(); + + /// In the case of multiple selections, + /// we need to return a new selection for each selected node individually. + /// + /// < > means selected. + /// text: abcdopqr + /// + if (!backwardSelection.isSingle) { + if (i == 0) { + newSelection = newSelection.copyWith(end: selectable.end()); + } else if (i == nodes.length - 1) { + newSelection = newSelection.copyWith(start: selectable.start()); } else { - newSelection = selection.copyWith( + newSelection = Selection( start: selectable.start(), end: selectable.end(), ); @@ -379,13 +375,13 @@ class _FlowySelectionState extends State } final rects = selectable.getRectsInSelection(newSelection); - for (final rect in rects) { - // FIXME: Need to compute more precise location. + // TODO: Need to compute more precise location. topmostRect ??= rect; layerLink ??= node.layerLink; - _rects.add(_transformRectToGlobal(selectable, rect)); + selectionRects.add(_transformRectToGlobal(selectable, rect)); + final overlay = OverlayEntry( builder: (context) => SelectionWidget( color: widget.selectionColor, @@ -393,11 +389,11 @@ class _FlowySelectionState extends State rect: rect, ), ); - _selectionOverlays.add(overlay); + _selectionAreas.add(overlay); } - index += 1; } - Overlay.of(context)?.insertAll(_selectionOverlays); + + Overlay.of(context)?.insertAll(_selectionAreas); if (topmostRect != null && layerLink != null) { editorState.service.toolbarService @@ -405,89 +401,141 @@ class _FlowySelectionState extends State } } + void _updateCursorAreas(Position position) { + final node = editorState.document.root.childAtPath(position.path); + + if (node == null) { + assert(false); + return; + } + + currentSelectedNodes = [node]; + + _showCursor(node, position); + } + + void _showCursor(Node node, Position position) { + final selectable = node.selectable; + final cursorRect = selectable?.getCursorRectInPosition(position); + if (selectable != null && cursorRect != null) { + final cursorArea = OverlayEntry( + builder: (context) => CursorWidget( + key: _cursorKey, + rect: cursorRect, + color: widget.cursorColor, + layerLink: node.layerLink, + ), + ); + + _cursorAreas.add(cursorArea); + selectionRects.add(_transformRectToGlobal(selectable, cursorRect)); + Overlay.of(context)?.insertAll(_cursorAreas); + + _forceShowCursor(); + } + } + + void _forceShowCursor() { + _cursorKey.currentState?.unwrapOrNull()?.show(); + } + + void _scrollUpOrDownIfNeeded() { + final dy = editorState.service.scrollService?.dy; + final selectNodes = currentSelectedNodes; + final selection = currentSelection.value; + if (dy == null || selection == null || selectNodes.isEmpty) { + return; + } + + final rect = selectNodes.last.rect; + + final size = MediaQuery.of(context).size.height; + final topLimit = size * 0.3; + final bottomLimit = size * 0.8; + + /// TODO: It is necessary to calculate the relative speed + /// according to the gap and move forward more gently. + if (rect.top >= bottomLimit) { + if (selection.isSingle) { + editorState.service.scrollService?.scrollTo(dy + size * 0.2); + } else if (selection.isBackward) { + editorState.service.scrollService?.scrollTo(dy + 10.0); + } + } else if (rect.bottom <= topLimit) { + if (selection.isForward) { + editorState.service.scrollService?.scrollTo(dy - 10.0); + } + } + } + + Node? _getNodeInOffset( + List sortedNodes, Offset offset, int start, int end) { + if (start < 0 && end >= sortedNodes.length) { + return null; + } + var min = start; + var max = end; + while (min <= max) { + final mid = min + ((max - min) >> 1); + final rect = sortedNodes[mid].rect; + if (rect.bottom <= offset.dy) { + min = mid + 1; + } else { + max = mid - 1; + } + } + final node = sortedNodes[min]; + if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) { + final children = node.children.toList(growable: false); + return _getNodeInOffset( + children, + offset, + 0, + children.length - 1, + ); + } + return node; + } + + void _enableInteraction() { + editorState.service.keyboardService?.enable(); + editorState.service.scrollService?.enable(); + } + Rect _transformRectToGlobal(Selectable selectable, Rect r) { final Offset topLeft = selectable.localToGlobal(Offset(r.left, r.top)); return Rect.fromLTWH(topLeft.dx, topLeft.dy, r.width, r.height); } - void _updateCursor(Position position) { - final node = editorState.document.root.childAtPath(position.path); - - assert(node != null); - if (node == null) { - return; - } - - currentSelectedNodes = [node]; - currentSelection.value = Selection.collapsed(position); - - final selectable = node.selectable; - final rect = selectable?.getCursorRectInPosition(position); - if (rect != null) { - _rects.add(_transformRectToGlobal(selectable!, rect)); - final cursor = OverlayEntry( - builder: (context) => CursorWidget( - key: _cursorKey, - rect: rect, - color: widget.cursorColor, - layerLink: node.layerLink, - ), - ); - _cursorOverlays.add(cursor); - Overlay.of(context)?.insertAll(_cursorOverlays); - _forceShowCursor(); - } + void _onSelectionChange() { + _scrollUpOrDownIfNeeded(); } - _forceShowCursor() { - final currentState = _cursorKey.currentState as CursorWidgetState?; - currentState?.show(); - } - - List _selectedNodesInSelection( - StateTree stateTree, Selection selection) { - final startNode = stateTree.nodeAtPath(selection.start.path)!; - final endNode = stateTree.nodeAtPath(selection.end.path)!; - return NodeIterator(stateTree, startNode, endNode).toList(); - } - - void _scrollUpOrDownIfNeeded(Offset offset, bool isDownward) { - final dy = editorState.service.scrollService?.dy; - if (dy == null) { - assert(false, 'Dy could not be null'); - return; - } - final topLimit = MediaQuery.of(context).size.height * 0.2; - final bottomLimit = MediaQuery.of(context).size.height * 0.8; - - /// TODO: It is necessary to calculate the relative speed - /// according to the gap and move forward more gently. - const distance = 10.0; - if (offset.dy <= topLimit && !isDownward) { - // up - editorState.service.scrollService?.scrollTo(dy - distance); - } else if (offset.dy >= bottomLimit && isDownward) { - //down - editorState.service.scrollService?.scrollTo(dy + distance); - } - } - - void _showDebugLayerIfNeeded() { + void _showDebugLayerIfNeeded({Offset? offset}) { // remove false to show debug overlay. if (kDebugMode && false) { _debugOverlay?.remove(); - if (panStartOffset != null) { + if (offset != null) { + _debugOverlay = OverlayEntry( + builder: (context) => Positioned.fromRect( + rect: Rect.fromPoints(offset, offset.translate(20, 20)), + child: Container( + color: Colors.red.withOpacity(0.2), + ), + ), + ); + Overlay.of(context)?.insert(_debugOverlay!); + } else if (_panStartOffset != null) { _debugOverlay = OverlayEntry( builder: (context) => Positioned.fromRect( rect: Rect.fromPoints( - panStartOffset?.translate( - 0, - -(editorState.service.scrollService!.dy - - panStartScrollDy!), - ) ?? - Offset.zero, - panEndOffset ?? Offset.zero) - .translate(0, 0), + _panStartOffset?.translate( + 0, + -(editorState.service.scrollService!.dy - + _panStartScrollDy!), + ) ?? + Offset.zero, + offset ?? Offset.zero), child: Container( color: Colors.red.withOpacity(0.2), ), @@ -499,175 +547,4 @@ class _FlowySelectionState extends State } } } - - Node _lowerBoundInDocument(Offset offset) { - final sortedNodes = - editorState.document.root.children.toList(growable: false); - return _lowerBound(sortedNodes, offset, 0, sortedNodes.length - 1); - } - - Node _upperBoundInDocument(Offset offset) { - final sortedNodes = - editorState.document.root.children.toList(growable: false); - return _upperBound(sortedNodes, offset, 0, sortedNodes.length - 1); - } - - /// TODO: Supports multi-level nesting, - /// currently only single-level nesting is supported - // find the first node's rect.bottom <= offset.dy - Node _lowerBound(List sortedNodes, Offset offset, int start, int end) { - assert(start >= 0 && end < sortedNodes.length); - var min = start; - var max = end; - while (min <= max) { - final mid = min + ((max - min) >> 1); - if (sortedNodes[mid].rect.bottom <= offset.dy) { - min = mid + 1; - } else { - max = mid - 1; - } - } - final node = sortedNodes[min]; - if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) { - final children = node.children.toList(growable: false); - return _lowerBound(children, offset, 0, children.length - 1); - } - return node; - } - - /// TODO: Supports multi-level nesting, - /// currently only single-level nesting is supported - // find the first node's rect.top < offset.dy - Node _upperBound( - List sortedNodes, - Offset offset, - int start, - int end, - ) { - assert(start >= 0 && end < sortedNodes.length); - var min = start; - var max = end; - while (min <= max) { - final mid = min + ((max - min) >> 1); - if (sortedNodes[mid].rect.top < offset.dy) { - min = mid + 1; - } else { - max = mid - 1; - } - } - final node = sortedNodes[max]; - if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) { - final children = node.children.toList(growable: false); - return _lowerBound(children, offset, 0, children.length - 1); - } - return node; - } -} - -/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] -/// for a while. So we need to implement our own GestureDetector. -@immutable -class _SelectionGestureDetector extends StatefulWidget { - const _SelectionGestureDetector( - {Key? key, - this.child, - this.onTapDown, - this.onDoubleTapDown, - this.onTripleTapDown, - this.onPanStart, - this.onPanUpdate, - this.onPanEnd}) - : super(key: key); - - @override - State<_SelectionGestureDetector> createState() => - _SelectionGestureDetectorState(); - - final Widget? child; - - final GestureTapDownCallback? onTapDown; - final GestureTapDownCallback? onDoubleTapDown; - final GestureTapDownCallback? onTripleTapDown; - final GestureDragStartCallback? onPanStart; - final GestureDragUpdateCallback? onPanUpdate; - final GestureDragEndCallback? onPanEnd; -} - -const Duration kTripleTapTimeout = Duration(milliseconds: 500); - -class _SelectionGestureDetectorState extends State<_SelectionGestureDetector> { - bool _isDoubleTap = false; - Timer? _doubleTapTimer; - int _tripleTabCount = 0; - Timer? _tripleTabTimer; - @override - Widget build(BuildContext context) { - return RawGestureDetector( - behavior: HitTestBehavior.translucent, - gestures: { - PanGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => PanGestureRecognizer(), - (recognizer) { - recognizer - ..onStart = widget.onPanStart - ..onUpdate = widget.onPanUpdate - ..onEnd = widget.onPanEnd; - }, - ), - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (recognizer) { - recognizer.onTapDown = _tapDownDelegate; - }, - ), - }, - child: widget.child, - ); - } - - _tapDownDelegate(TapDownDetails tapDownDetails) { - if (_tripleTabCount == 2) { - _tripleTabCount = 0; - _tripleTabTimer?.cancel(); - _tripleTabTimer = null; - if (widget.onTripleTapDown != null) { - widget.onTripleTapDown!(tapDownDetails); - } - } else if (_isDoubleTap) { - _isDoubleTap = false; - _doubleTapTimer?.cancel(); - _doubleTapTimer = null; - if (widget.onDoubleTapDown != null) { - widget.onDoubleTapDown!(tapDownDetails); - } - _tripleTabCount++; - } else { - if (widget.onTapDown != null) { - widget.onTapDown!(tapDownDetails); - } - - _isDoubleTap = true; - _doubleTapTimer?.cancel(); - _doubleTapTimer = Timer(kDoubleTapTimeout, () { - _isDoubleTap = false; - _doubleTapTimer = null; - }); - - _tripleTabCount = 1; - _tripleTabTimer?.cancel(); - _tripleTabTimer = Timer(kTripleTapTimeout, () { - _tripleTabCount = 0; - _tripleTabTimer = null; - }); - } - } - - @override - void dispose() { - _doubleTapTimer?.cancel(); - _tripleTabTimer?.cancel(); - super.dispose(); - } } From 1bdd863b75cbd027530ea5b6cec80f22827c9f21 Mon Sep 17 00:00:00 2001 From: appflowy Date: Wed, 10 Aug 2022 20:12:05 +0800 Subject: [PATCH 061/224] refactor: refactor multi-select typeOption --- .../plugins/board/application/board_bloc.dart | 8 ++ .../cell/cell_service/cell_service.dart | 2 +- .../application/field/field_editor_bloc.dart | 4 +- .../field/field_type_option_edit_bloc.dart | 1 + .../field/type_option/date_bloc.dart | 9 +- .../type_option/multi_select_type_option.dart | 9 +- .../field/type_option/number_bloc.dart | 10 +- .../single_select_type_option.dart | 34 +++-- .../type_option_data_controller.dart | 117 +----------------- .../widgets/cell/date_cell/date_editor.dart | 1 + .../widgets/header/field_cell.dart | 2 +- .../widgets/header/field_editor.dart | 2 +- .../widgets/header/grid_header.dart | 2 +- .../widgets/header/type_option/builder.dart | 5 +- .../widgets/header/type_option/checkbox.dart | 13 +- .../widgets/header/type_option/date.dart | 1 + .../widgets/header/type_option/number.dart | 1 + .../widgets/header/type_option/rich_text.dart | 11 +- .../header/type_option/single_select.dart | 17 ++- .../widgets/header/type_option/url.dart | 11 +- .../presentation/widgets/row/row_detail.dart | 2 +- .../widgets/toolbar/grid_property.dart | 2 +- 22 files changed, 58 insertions(+), 206 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 2cb03a6c64..5bf941ef99 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -56,6 +56,9 @@ class BoardBloc extends Bloc { didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, + groupByField: (GridFieldPB field) { + emit(state.copyWith(groupField: Some(field))); + }, ); }, ); @@ -97,11 +100,16 @@ class BoardBloc extends Bloc { } void _buildColumns(UnmodifiableListView fields) { + GridFieldPB? groupField; for (final field in fields) { if (field.fieldType == FieldType.SingleSelect) { + groupField = field; _buildColumnsFromSingleSelect(field); } } + + assert(groupField != null); + add(BoardEvent.groupByField(groupField!)); } void _buildColumnsFromSingleSelect(GridFieldPB field) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart index 78b6551f0f..7dc15a01ee 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart @@ -17,7 +17,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'dart:convert' show utf8; import '../../field/field_cache.dart'; -import '../../field/type_option/type_option_data_controller.dart'; +import '../../field/type_option/type_option_context.dart'; import 'cell_field_notifier.dart'; part 'cell_service.freezed.dart'; part 'cell_data_loader.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart index 05c61eaf55..fc80964a87 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart @@ -2,9 +2,11 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:async'; import 'package:dartz/dartz.dart'; -import 'type_option/type_option_data_controller.dart'; +import 'type_option/type_option_context.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'type_option/type_option_data_controller.dart'; + part 'field_editor_bloc.freezed.dart'; class FieldEditorBloc extends Bloc { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart index ec18c7e5b5..5aa380cd72 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart @@ -2,6 +2,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; + import 'type_option/type_option_data_controller.dart'; part 'field_type_option_edit_bloc.freezed.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart index 1b9c3841ff..2b2a853bba 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart @@ -5,16 +5,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; -import 'type_option_data_controller.dart'; +import 'type_option_context.dart'; part 'date_bloc.freezed.dart'; -class DateTypeOptionDataParser extends TypeOptionDataParser { - @override - DateTypeOption fromBuffer(List buffer) { - return DateTypeOption.fromBuffer(buffer); - } -} - class DateTypeOptionBloc extends Bloc { DateTypeOptionBloc({required DateTypeOptionContext typeOptionContext}) diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart index acf46a0b37..1e99859a27 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart @@ -3,6 +3,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'dart:async'; import 'select_option_type_option_bloc.dart'; +import 'type_option_context.dart'; import 'type_option_data_controller.dart'; import 'type_option_service.dart'; import 'package:protobuf/protobuf.dart'; @@ -72,11 +73,3 @@ class MultiSelectTypeOptionContext }; } } - -class MultiSelectTypeOptionWidgetDataParser - extends TypeOptionDataParser { - @override - MultiSelectTypeOption fromBuffer(List buffer) { - return MultiSelectTypeOption.fromBuffer(buffer); - } -} diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart index 7b723324bf..d53c5b9cda 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart @@ -4,18 +4,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; -import 'type_option_data_controller.dart'; +import 'type_option_context.dart'; part 'number_bloc.freezed.dart'; -class NumberTypeOptionWidgetDataParser - extends TypeOptionDataParser { - @override - NumberTypeOption fromBuffer(List buffer) { - return NumberTypeOption.fromBuffer(buffer); - } -} - class NumberTypeOptionBloc extends Bloc { NumberTypeOptionBloc({required NumberTypeOptionContext typeOptionContext}) diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart index d8f728f1b5..8ba03c0bc7 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/single_select_type_option.dart @@ -4,20 +4,26 @@ import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart' import 'dart:async'; import 'package:protobuf/protobuf.dart'; import 'select_option_type_option_bloc.dart'; -import 'type_option_data_controller.dart'; +import 'type_option_context.dart'; import 'type_option_service.dart'; -class SingleSelectTypeOptionContext with ISelectOptionAction { +class SingleSelectAction with ISelectOptionAction { + final String gridId; + final String fieldId; + final SingleSelectTypeOptionContext typeOptionContext; final TypeOptionFFIService service; - SingleSelectTypeOptionContext({ - required SingleSelectTypeOptionWidgetDataParser dataBuilder, - required TypeOptionDataController dataController, - }) : service = TypeOptionFFIService( - gridId: dataController.gridId, - fieldId: dataController.field.id, - ), - super(dataParser: dataBuilder, dataController: dataController); + SingleSelectAction({ + required this.gridId, + required this.fieldId, + required this.typeOptionContext, + }) : service = TypeOptionFFIService(gridId: gridId, fieldId: fieldId); + + SingleSelectTypeOptionPB get typeOption => typeOptionContext.typeOption; + + set typeOption(SingleSelectTypeOptionPB newTypeOption) { + typeOptionContext.typeOption = newTypeOption; + } @override List Function(SelectOptionPB) get deleteOption { @@ -71,11 +77,3 @@ class SingleSelectTypeOptionContext with ISelectOptionAction { }; } } - -class SingleSelectTypeOptionWidgetDataParser - extends TypeOptionDataParser { - @override - SingleSelectTypeOptionPB fromBuffer(List buffer) { - return SingleSelectTypeOptionPB.fromBuffer(buffer); - } -} diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart index 424abc87ce..b28cdcdb7d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart @@ -1,127 +1,12 @@ import 'package:flowy_infra/notifier.dart'; -import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:dartz/dartz.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:protobuf/protobuf.dart'; import 'package:flowy_sdk/log.dart'; -abstract class TypeOptionDataParser { - T fromBuffer(List buffer); -} - -typedef NumberTypeOptionContext = TypeOptionContext; -typedef RichTextTypeOptionContext = TypeOptionContext; -typedef CheckboxTypeOptionContext = TypeOptionContext; -typedef URLTypeOptionContext = TypeOptionContext; -typedef DateTypeOptionContext = TypeOptionContext; - -typedef SingleSelectTypeOptionContext - = TypeOptionContext; - -class TypeOptionContext { - T? _typeOptionObject; - final TypeOptionDataParser dataParser; - final TypeOptionDataController _dataController; - - TypeOptionContext({ - required this.dataParser, - required TypeOptionDataController dataController, - }) : _dataController = dataController; - - String get gridId => _dataController.gridId; - - Future loadTypeOptionData({ - required void Function(T) onCompleted, - required void Function(FlowyError) onError, - }) async { - await _dataController.loadTypeOptionData().then((result) { - result.fold((l) => null, (err) => onError(err)); - }); - - onCompleted(typeOption); - } - - T get typeOption { - if (_typeOptionObject != null) { - return _typeOptionObject!; - } - - final T object = _dataController.getTypeOption(dataParser); - _typeOptionObject = object; - return object; - } - - set typeOption(T typeOption) { - _dataController.typeOptionData = typeOption.writeToBuffer(); - _typeOptionObject = typeOption; - } -} - -abstract class TypeOptionFieldDelegate { - void onFieldChanged(void Function(String) callback); - void dispose(); -} - -abstract class IFieldTypeOptionLoader { - String get gridId; - Future> load(); - - Future> switchToField( - String fieldId, FieldType fieldType) { - final payload = EditFieldPayloadPB.create() - ..gridId = gridId - ..fieldId = fieldId - ..fieldType = fieldType; - - return GridEventSwitchToField(payload).send(); - } -} - -class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { - @override - final String gridId; - NewFieldTypeOptionLoader({ - required this.gridId, - }); - - @override - Future> load() { - final payload = CreateFieldPayloadPB.create() - ..gridId = gridId - ..fieldType = FieldType.RichText; - - return GridEventCreateFieldTypeOption(payload).send(); - } -} - -class FieldTypeOptionLoader extends IFieldTypeOptionLoader { - @override - final String gridId; - final GridFieldPB field; - - FieldTypeOptionLoader({ - required this.gridId, - required this.field, - }); - - @override - Future> load() { - final payload = GridFieldTypeOptionIdPB.create() - ..gridId = gridId - ..fieldId = field.id - ..fieldType = field.fieldType; - - return GridEventGetFieldTypeOption(payload).send(); - } -} +import 'type_option_context.dart'; class TypeOptionDataController { final String gridId; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart index a439506815..bfcc36439c 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart index f66d5bf6d8..cb8df60591 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart @@ -1,6 +1,6 @@ import 'package:app_flowy/plugins/grid/application/field/field_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart index 1a9a0ead83..efa70bb81a 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart @@ -1,5 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart'; -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart index cad7e300f8..4b5f364c0d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart @@ -1,5 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index d89dc26a68..8a6edcbf2c 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:app_flowy/plugins/grid/application/field/type_option/multi_select_type_option.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; @@ -86,7 +87,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ gridId: gridId, fieldType: fieldType, dataController: dataController, - ) as SingleSelectTypeOptionContext, + ), overlayDelegate, ); case FieldType.MultiSelect: @@ -165,7 +166,7 @@ TypeOptionContext case FieldType.SingleSelect: return SingleSelectTypeOptionContext( dataController: dataController, - dataBuilder: SingleSelectTypeOptionWidgetDataParser(), + dataParser: SingleSelectTypeOptionWidgetDataParser(), ) as TypeOptionContext; case FieldType.MultiSelect: return MultiSelectTypeOptionContext( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart index fc4c4c16a7..92511a888a 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/checkbox.dart @@ -1,18 +1,7 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -typedef CheckboxTypeOptionContext = TypeOptionContext; - -class CheckboxTypeOptionWidgetDataParser - extends TypeOptionDataParser { - @override - CheckboxTypeOption fromBuffer(List buffer) { - return CheckboxTypeOption.fromBuffer(buffer); - } -} - class CheckboxTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { CheckboxTypeOptionWidgetBuilder(CheckboxTypeOptionContext typeOptionContext); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart index c162595b88..51433037a3 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/date_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:easy_localization/easy_localization.dart' hide DateFormat; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart index 8e4e53b40c..d15be4a6a0 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/number_bloc.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/number_format_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart index c24f98a28f..1ca0386c31 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/rich_text.dart @@ -1,16 +1,7 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -class RichTextTypeOptionWidgetDataParser - extends TypeOptionDataParser { - @override - RichTextTypeOption fromBuffer(List buffer) { - return RichTextTypeOption.fromBuffer(buffer); - } -} - class RichTextTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { RichTextTypeOptionWidgetBuilder(RichTextTypeOptionContext typeOptionContext); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/single_select.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/single_select.dart index 57940ecbee..d9d699fdff 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/single_select.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/single_select.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/single_select_type_option.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flutter/material.dart'; import '../field_type_option_editor.dart'; import 'builder.dart'; @@ -8,10 +9,14 @@ class SingleSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final SingleSelectTypeOptionWidget _widget; SingleSelectTypeOptionWidgetBuilder( - SingleSelectTypeOptionContext typeOptionContext, + SingleSelectTypeOptionContext singleSelectTypeOption, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = SingleSelectTypeOptionWidget( - typeOptionContext: typeOptionContext, + selectOptionAction: SingleSelectAction( + fieldId: singleSelectTypeOption.fieldId, + gridId: singleSelectTypeOption.gridId, + typeOptionContext: singleSelectTypeOption, + ), overlayDelegate: overlayDelegate, ); @@ -20,11 +25,11 @@ class SingleSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { } class SingleSelectTypeOptionWidget extends TypeOptionWidget { - final SingleSelectTypeOptionContext typeOptionContext; + final SingleSelectAction selectOptionAction; final TypeOptionOverlayDelegate overlayDelegate; const SingleSelectTypeOptionWidget({ - required this.typeOptionContext, + required this.selectOptionAction, required this.overlayDelegate, Key? key, }) : super(key: key); @@ -32,10 +37,10 @@ class SingleSelectTypeOptionWidget extends TypeOptionWidget { @override Widget build(BuildContext context) { return SelectOptionTypeOptionWidget( - options: typeOptionContext.typeOption.options, + options: selectOptionAction.typeOption.options, beginEdit: () => overlayDelegate.hideOverlay(context), overlayDelegate: overlayDelegate, - typeOptionAction: typeOptionContext, + typeOptionAction: selectOptionAction, // key: ValueKey(state.typeOption.hashCode), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart index 07a341493a..9997837d63 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/url.dart @@ -1,16 +1,7 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flutter/material.dart'; import 'builder.dart'; -class URLTypeOptionWidgetDataParser - extends TypeOptionDataParser { - @override - URLTypeOption fromBuffer(List buffer) { - return URLTypeOption.fromBuffer(buffer); - } -} - class URLTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { URLTypeOptionWidgetBuilder(URLTypeOptionContext typeOptionContext); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index 3412d54e1e..a98fcfa688 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -1,5 +1,5 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_detail_bloc.dart'; import 'package:flowy_infra/image.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index 290da43f95..b49cce1f11 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart @@ -1,4 +1,4 @@ -import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/setting/property_bloc.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart'; From 5c011fbd7ec8469c1da635f72ba2c275f3ec8752 Mon Sep 17 00:00:00 2001 From: Victor Teles Date: Wed, 10 Aug 2022 11:46:29 -0300 Subject: [PATCH 062/224] refactor: added Platform.isMacOS to validation --- .../lib/workspace/presentation/home/home_layout.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart index 94cb1874cd..5194b9edb7 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/time/duration.dart'; @@ -38,7 +40,8 @@ class HomeLayout { } homePageLOffset = showMenu ? menuWidth : 0.0; - menuSpacing = showMenu ? 0 : 80.0; + + menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0; animDuration = .35.seconds; editPanelWidth = HomeSizes.editPanelWidth; From 29ea3c83c8a558b35c5fcedf36f9cbb9e3523e7f Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 11 Aug 2022 10:08:42 +0800 Subject: [PATCH 063/224] chore: refactor grid setting --- .../group.rs} | 58 +++++- .../src/entities/group_entities/mod.rs | 3 + .../src/entities/setting_entities.rs | 8 +- .../flowy-grid/src/services/grid_editor.rs | 4 + .../src/services/grid_editor_task.rs | 1 + .../src/services/group/group_service.rs | 7 + .../flowy-grid/src/services/group/mod.rs | 3 + .../rust-lib/flowy-grid/src/services/mod.rs | 1 + .../flowy-grid/src/services/tasks/queue.rs | 1 + .../flowy-grid/src/services/tasks/task.rs | 9 + frontend/rust-lib/flowy-grid/src/util.rs | 24 +-- frontend/rust-lib/flowy-test/src/helper.rs | 1 + .../src/revision/filter_rev.rs | 12 ++ .../src/revision/grid_filter_rev.rs | 94 --------- .../src/revision/grid_setting_rev.rs | 190 ++++++++++-------- .../src/revision/group_rev.rs | 11 + .../flowy-grid-data-model/src/revision/mod.rs | 4 + .../src/client_grid/grid_revision_pad.rs | 30 ++- shared-lib/flowy-sync/src/entities/grid.rs | 13 +- 19 files changed, 252 insertions(+), 222 deletions(-) rename frontend/rust-lib/flowy-grid/src/entities/{group_entities.rs => group_entities/group.rs} (57%) create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_service.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/mod.rs create mode 100644 shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs delete mode 100644 shared-lib/flowy-grid-data-model/src/revision/grid_filter_rev.rs create mode 100644 shared-lib/flowy-grid-data-model/src/revision/group_rev.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs similarity index 57% rename from frontend/rust-lib/flowy-grid/src/entities/group_entities.rs rename to frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index dda624fc67..d05c2ad966 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -1,8 +1,9 @@ +use crate::entities::FieldType; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::GridGroupRevision; -use flowy_sync::entities::grid::CreateGridGroupParams; +use flowy_sync::entities::grid::{CreateGridGroupParams, DeleteGroupParams}; use std::convert::TryInto; use std::sync::Arc; @@ -11,8 +12,8 @@ pub struct GridGroupPB { #[pb(index = 1)] pub id: String, - #[pb(index = 2, one_of)] - pub group_field_id: Option, + #[pb(index = 2)] + pub group_field_id: String, #[pb(index = 3, one_of)] pub sub_group_field_id: Option, @@ -50,27 +51,64 @@ impl std::convert::From>> for RepeatedGridGroupPB { #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct CreateGridGroupPayloadPB { - #[pb(index = 1, one_of)] - pub field_id: Option, + #[pb(index = 1)] + pub field_id: String, #[pb(index = 2, one_of)] pub sub_field_id: Option, + + #[pb(index = 3)] + pub field_type: FieldType, } impl TryInto for CreateGridGroupPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { - let field_id = match self.field_id { - None => None, - Some(field_id) => Some(NotEmptyStr::parse(field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), - }; + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; let sub_field_id = match self.sub_field_id { None => None, Some(field_id) => Some(NotEmptyStr::parse(field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), }; - Ok(CreateGridGroupParams { field_id, sub_field_id }) + Ok(CreateGridGroupParams { + field_id, + sub_field_id, + field_type_rev: self.field_type.into(), + }) + } +} + +#[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, +} + +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; + + Ok(DeleteGroupParams { + field_id, + field_type_rev: self.field_type.into(), + group_id, + }) } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs new file mode 100644 index 0000000000..b50e59f6d6 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs @@ -0,0 +1,3 @@ +mod group; + +pub use group::*; diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 3564886c4a..7828c144f0 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -1,12 +1,12 @@ use crate::entities::{ CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, CreateGridSortPayloadPB, DeleteFilterPayloadPB, - RepeatedGridFilterPB, RepeatedGridGroupPB, RepeatedGridSortPB, + DeleteGroupPayloadPB, RepeatedGridFilterPB, RepeatedGridGroupPB, RepeatedGridSortPB, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::GridLayoutRevision; -use flowy_sync::entities::grid::GridSettingChangesetParams; +use flowy_sync::entities::grid::{DeleteGroupParams, GridSettingChangesetParams}; use std::collections::HashMap; use std::convert::TryInto; use strum::IntoEnumIterator; @@ -97,7 +97,7 @@ pub struct GridSettingChangesetPayloadPB { pub insert_group: Option, #[pb(index = 6, one_of)] - pub delete_group: Option, + pub delete_group: Option, #[pb(index = 7, one_of)] pub insert_sort: Option, @@ -130,8 +130,8 @@ impl TryInto for GridSettingChangesetPayloadPB { }; let delete_group = match self.delete_group { + Some(payload) => Some(payload.try_into()?), None => None, - Some(filter_id) => Some(NotEmptyStr::parse(filter_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), }; let insert_sort = match self.insert_sort { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 08c7284b74..d98c9538d3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -6,6 +6,7 @@ use crate::services::block_manager::GridBlockManager; use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, CellBytes}; use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder}; use crate::services::filter::{GridFilterChangeset, GridFilterService}; +use crate::services::group::GridGroupService; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{ make_grid_blocks, make_row_from_row_rev, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder, @@ -34,6 +35,7 @@ pub struct GridRevisionEditor { block_manager: Arc, #[allow(dead_code)] pub(crate) filter_service: Arc, + pub(crate) group_service: Arc, } impl Drop for GridRevisionEditor { @@ -59,6 +61,7 @@ impl GridRevisionEditor { let block_manager = Arc::new(GridBlockManager::new(grid_id, &user, block_meta_revs, persistence).await?); let filter_service = Arc::new(GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await); + let group_service = Arc::new(GridGroupService::new()); let editor = Arc::new(Self { grid_id: grid_id.to_owned(), user, @@ -66,6 +69,7 @@ impl GridRevisionEditor { rev_manager, block_manager, filter_service, + group_service, }); Ok(editor) diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs index 0338730818..f5c45811dd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs @@ -19,6 +19,7 @@ impl GridTaskHandler for GridRevisionEditor { Box::pin(async move { match content { TaskContent::Snapshot => {} + TaskContent::Group => {} TaskContent::Filter(context) => self.filter_service.process(context).await?, } Ok(()) diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs new file mode 100644 index 0000000000..255ec015d6 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -0,0 +1,7 @@ +pub struct GridGroupService {} + +impl GridGroupService { + pub fn new() -> Self { + Self {} + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs new file mode 100644 index 0000000000..c1f767b62e --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -0,0 +1,3 @@ +mod group_service; + +pub use group_service::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/mod.rs b/frontend/rust-lib/flowy-grid/src/services/mod.rs index 6e670bae8c..dc45575ab3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/mod.rs @@ -7,6 +7,7 @@ pub mod field; mod filter; pub mod grid_editor; mod grid_editor_task; +pub mod group; pub mod persistence; pub mod row; pub mod setting; diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs index 1ba97afd9a..a20fdab7c6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs @@ -27,6 +27,7 @@ impl GridTaskQueue { let task_type = match task.content.as_ref().unwrap() { TaskContent::Snapshot => TaskType::Snapshot, + TaskContent::Group => TaskType::Group, TaskContent::Filter { .. } => TaskType::Filter, }; let pending_task = PendingTask { diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs index 92950b02aa..6b88e4598f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs @@ -10,6 +10,8 @@ pub enum TaskType { Filter, /// Generate snapshot for grid, unused by now. Snapshot, + + Group, } impl PartialEq for TaskType { @@ -44,9 +46,15 @@ impl PartialOrd for PendingTask { impl Ord for PendingTask { fn cmp(&self, other: &Self) -> Ordering { match (self.ty, other.ty) { + // Snapshot (TaskType::Snapshot, TaskType::Snapshot) => Ordering::Equal, (TaskType::Snapshot, _) => Ordering::Greater, (_, TaskType::Snapshot) => Ordering::Less, + // Group + (TaskType::Group, TaskType::Group) => self.id.cmp(&other.id).reverse(), + (TaskType::Group, _) => Ordering::Greater, + (_, TaskType::Group) => Ordering::Greater, + // Filter (TaskType::Filter, TaskType::Filter) => self.id.cmp(&other.id).reverse(), } } @@ -59,6 +67,7 @@ pub(crate) struct FilterTaskContext { pub(crate) enum TaskContent { #[allow(dead_code)] Snapshot, + Group, Filter(FilterTaskContext), } diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index a53314a812..7f2d3e48ab 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -54,24 +54,14 @@ pub fn make_default_board() -> BuildGridContext { let single_select_field_id = single_select_field.id.clone(); grid_builder.add_field(single_select_field); - let field_revs = grid_builder.field_revs(); - let block_id = grid_builder.block_id(); - // rows - let row_1 = RowRevisionBuilder::new(block_id, field_revs) - .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) - .build(); - grid_builder.add_row(row_1); - - let row_2 = RowRevisionBuilder::new(block_id, field_revs) - .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) - .build(); - grid_builder.add_row(row_2); - - let row_3 = RowRevisionBuilder::new(block_id, field_revs) - .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) - .build(); - grid_builder.add_row(row_3); + for _ in 0..3 { + grid_builder.add_row( + RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()) + .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) + .build(), + ); + } grid_builder.build() } diff --git a/frontend/rust-lib/flowy-test/src/helper.rs b/frontend/rust-lib/flowy-test/src/helper.rs index 6254955ceb..d7155965e8 100644 --- a/frontend/rust-lib/flowy-test/src/helper.rs +++ b/frontend/rust-lib/flowy-test/src/helper.rs @@ -97,6 +97,7 @@ async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataTypePB desc: "".to_string(), thumbnail: Some("http://1.png".to_string()), data_type, + sub_data_type: None, plugin_type: 0, data, }; diff --git a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs new file mode 100644 index 0000000000..abf129c415 --- /dev/null +++ b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs @@ -0,0 +1,12 @@ +use crate::revision::FieldTypeRevision; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] +pub struct GridFilterRevision { + pub id: String, + pub field_id: String, + pub condition: u8, + pub content: Option, +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_filter_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_filter_rev.rs deleted file mode 100644 index 540a67b9ea..0000000000 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_filter_rev.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::entities::NumberFilterCondition; -use indexmap::IndexMap; -use nanoid::nanoid; -use serde::{Deserialize, Serialize}; -use serde_repr::*; -use std::str::FromStr; - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct GridFilterRevision { - pub id: String, - pub field_id: String, - pub condition: u8, - pub content: Option, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum TextFilterConditionRevision { - Is = 0, - IsNot = 1, - Contains = 2, - DoesNotContain = 3, - StartsWith = 4, - EndsWith = 5, - IsEmpty = 6, - IsNotEmpty = 7, -} - -impl ToString for TextFilterConditionRevision { - fn to_string(&self) -> String { - (self.clone() as u8).to_string() - } -} - -impl FromStr for TextFilterConditionRevision { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - let rev = serde_json::from_str(s)?; - Ok(rev) - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum NumberFilterConditionRevision { - Equal = 0, - NotEqual = 1, - GreaterThan = 2, - LessThan = 3, - GreaterThanOrEqualTo = 4, - LessThanOrEqualTo = 5, - IsEmpty = 6, - IsNotEmpty = 7, -} - -impl ToString for NumberFilterConditionRevision { - fn to_string(&self) -> String { - (self.clone() as u8).to_string() - } -} - -impl FromStr for NumberFilterConditionRevision { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - let rev = serde_json::from_str(s)?; - Ok(rev) - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum SelectOptionConditionRevision { - OptionIs = 0, - OptionIsNot = 1, - OptionIsEmpty = 2, - OptionIsNotEmpty = 3, -} - -impl ToString for SelectOptionConditionRevision { - fn to_string(&self) -> String { - (self.clone() as u8).to_string() - } -} - -impl FromStr for SelectOptionConditionRevision { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - let rev = serde_json::from_str(s)?; - Ok(rev) - } -} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index d29d9d767b..095b0dcd97 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -1,9 +1,12 @@ +use crate::revision::filter_rev::GridFilterRevision; +use crate::revision::group_rev::GridGroupRevision; use crate::revision::{FieldRevision, FieldTypeRevision}; use indexmap::IndexMap; use nanoid::nanoid; use serde::{Deserialize, Serialize}; use serde_repr::*; use std::collections::HashMap; +use std::fmt::Debug; use std::sync::Arc; pub fn gen_grid_filter_id() -> String { @@ -18,41 +21,50 @@ pub fn gen_grid_sort_id() -> String { nanoid!(6) } -/// Each layout contains multiple key/value. -/// Key: field_id -/// Value: this value also contains key/value. -/// Key: FieldType, -/// Value: the corresponding filter. -/// -/// This overall struct is described below: -/// GridSettingRevision -/// layout: -/// field_id: -/// FieldType: GridFilterRevision -/// FieldType: GridFilterRevision -/// field_id: -/// FieldType: GridFilterRevision -/// FieldType: GridFilterRevision -/// layout: -/// field_id: -/// FieldType: GridFilterRevision -/// FieldType: GridFilterRevision -/// -/// Group and sorts will be the same structure as filters. #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] pub struct GridSettingRevision { pub layout: GridLayoutRevision, + /// Each layout contains multiple key/value. + /// Key: field_id + /// Value: this value contains key/value. + /// Key: FieldType, + /// Value: the corresponding filters. #[serde(with = "indexmap::serde_seq")] filters: IndexMap>, + /// Each layout contains multiple key/value. + /// Key: field_id + /// Value: this value contains key/value. + /// Key: FieldType, + /// Value: the corresponding groups. #[serde(skip, with = "indexmap::serde_seq")] - pub groups: IndexMap>, + pub groups: IndexMap>, #[serde(skip, with = "indexmap::serde_seq")] pub sorts: IndexMap>, } +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum GridLayoutRevision { + Table = 0, + Board = 1, +} + +impl ToString for GridLayoutRevision { + fn to_string(&self) -> String { + let layout_rev = self.clone() as u8; + layout_rev.to_string() + } +} + +impl std::default::Default for GridLayoutRevision { + fn default() -> Self { + GridLayoutRevision::Table + } +} + pub type FiltersByFieldId = HashMap>>; pub type GroupsByFieldId = HashMap>>; pub type SortsByFieldId = HashMap>>; @@ -61,6 +73,36 @@ impl GridSettingRevision { None } + pub fn get_mut_groups( + &mut self, + layout: &GridLayoutRevision, + field_id: &str, + field_type: &FieldTypeRevision, + ) -> Option<&mut Vec>> { + self.groups + .get_mut(layout) + .and_then(|group_rev_map_by_field_id| group_rev_map_by_field_id.get_mut(field_id)) + .and_then(|group_rev_map| group_rev_map.get_mut(field_type)) + } + + pub fn insert_group( + &mut self, + layout: &GridLayoutRevision, + field_id: &str, + field_type: &FieldTypeRevision, + filter_rev: GridGroupRevision, + ) { + let filter_rev_map_by_field_id = self.groups.entry(layout.clone()).or_insert_with(IndexMap::new); + let filter_rev_map = filter_rev_map_by_field_id + .entry(field_id.to_string()) + .or_insert_with(GridGroupRevisionMap::new); + + filter_rev_map + .entry(field_type.to_owned()) + .or_insert_with(Vec::new) + .push(Arc::new(filter_rev)) + } + pub fn get_all_sort(&self) -> Option { None } @@ -135,70 +177,50 @@ impl GridSettingRevision { } } -#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] -#[serde(transparent)] -pub struct GridFilterRevisionMap { - #[serde(with = "indexmap::serde_seq")] - pub filter_by_field_type: IndexMap>>, -} - -impl GridFilterRevisionMap { - pub fn new() -> Self { - GridFilterRevisionMap::default() - } -} - -impl std::ops::Deref for GridFilterRevisionMap { - type Target = IndexMap>>; - - fn deref(&self) -> &Self::Target { - &self.filter_by_field_type - } -} - -impl std::ops::DerefMut for GridFilterRevisionMap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.filter_by_field_type - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum GridLayoutRevision { - Table = 0, - Board = 1, -} - -impl ToString for GridLayoutRevision { - fn to_string(&self) -> String { - let layout_rev = self.clone() as u8; - layout_rev.to_string() - } -} - -impl std::default::Default for GridLayoutRevision { - fn default() -> Self { - GridLayoutRevision::Table - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] -pub struct GridFilterRevision { - pub id: String, - pub field_id: String, - pub condition: u8, - pub content: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct GridGroupRevision { - pub id: String, - pub field_id: Option, - pub sub_field_id: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct GridSortRevision { pub id: String, pub field_id: Option, } + +pub type GridFilterRevisionMap = GridObjectRevisionMap; +pub type GridGroupRevisionMap = GridObjectRevisionMap; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +#[serde(transparent)] +pub struct GridObjectRevisionMap +where + T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, +{ + #[serde(with = "indexmap::serde_seq")] + pub object_by_field_type: IndexMap>>, +} + +impl GridObjectRevisionMap +where + T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, +{ + pub fn new() -> Self { + GridObjectRevisionMap::default() + } +} + +impl std::ops::Deref for GridObjectRevisionMap +where + T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, +{ + type Target = IndexMap>>; + + fn deref(&self) -> &Self::Target { + &self.object_by_field_type + } +} + +impl std::ops::DerefMut for GridObjectRevisionMap +where + T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.object_by_field_type + } +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs new file mode 100644 index 0000000000..e856354f6c --- /dev/null +++ b/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs @@ -0,0 +1,11 @@ +use crate::revision::FieldTypeRevision; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +pub struct GridGroupRevision { + pub id: String, + pub field_id: String, + pub sub_field_id: Option, +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/mod.rs b/shared-lib/flowy-grid-data-model/src/revision/mod.rs index a6581ad181..9f4b34300e 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/mod.rs @@ -1,5 +1,9 @@ +mod filter_rev; mod grid_rev; mod grid_setting_rev; +mod group_rev; +pub use filter_rev::*; pub use grid_rev::*; pub use grid_setting_rev::*; +pub use group_rev::*; diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index ffe1e81e9f..0d793aff47 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -409,24 +409,32 @@ impl GridRevisionPad { } } if let Some(params) = changeset.insert_group { - let rev = GridGroupRevision { + let group_rev = GridGroupRevision { id: gen_grid_group_id(), - field_id: params.field_id, + field_id: params.field_id.clone(), sub_field_id: params.sub_field_id, }; grid_rev .setting - .groups - .entry(layout_rev.clone()) - .or_insert_with(std::vec::Vec::new) - .push(rev); - - is_changed = Some(()) + .insert_group(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev, group_rev); + is_changed = Some(()); } - if let Some(delete_group_id) = changeset.delete_group { - match grid_rev.setting.groups.get_mut(&layout_rev) { - Some(groups) => groups.retain(|group| group.id != delete_group_id), + if let Some(params) = changeset.delete_group { + // match grid_rev.setting.groups.get_mut(&layout_rev) { + // Some(groups) => groups.retain(|group| group.id != delete_group_id), + // None => { + // tracing::warn!("Can't find the group with {:?}", layout_rev); + // } + // } + + match grid_rev + .setting + .get_mut_groups(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) + { + Some(groups) => { + groups.retain(|filter| filter.id != params.group_id); + } None => { tracing::warn!("Can't find the group with {:?}", layout_rev); } diff --git a/shared-lib/flowy-sync/src/entities/grid.rs b/shared-lib/flowy-sync/src/entities/grid.rs index fc3c14b4fb..8b5e9d4629 100644 --- a/shared-lib/flowy-sync/src/entities/grid.rs +++ b/shared-lib/flowy-sync/src/entities/grid.rs @@ -6,7 +6,7 @@ pub struct GridSettingChangesetParams { pub insert_filter: Option, pub delete_filter: Option, pub insert_group: Option, - pub delete_group: Option, + pub delete_group: Option, pub insert_sort: Option, pub delete_sort: Option, } @@ -28,10 +28,19 @@ pub struct DeleteFilterParams { pub filter_id: String, pub field_type_rev: FieldTypeRevision, } + pub struct CreateGridGroupParams { - pub field_id: Option, + pub field_id: String, pub sub_field_id: Option, + pub field_type_rev: FieldTypeRevision, } + +pub struct DeleteGroupParams { + pub field_id: String, + pub group_id: String, + pub field_type_rev: FieldTypeRevision, +} + pub struct CreateGridSortParams { pub field_id: Option, } From 9b00a25004eadf4a511f9914d100298be9ce9195 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 10:59:59 +0800 Subject: [PATCH 064/224] fix: the height of the selection rects in same line is not same --- .../flowy_editor/example/lib/main.dart | 6 ----- .../src/render/rich_text/flowy_rich_text.dart | 24 +++++++++++++------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 5db7288c76..19c732f6a0 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -21,7 +21,6 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // @@ -64,11 +63,6 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), body: _buildBody(), floatingActionButton: _buildExpandableFab(), ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index fb0009ad73..01949aa99c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -17,7 +19,7 @@ class FlowyRichText extends StatefulWidget { const FlowyRichText({ Key? key, this.cursorHeight, - this.cursorWidth = 2.0, + this.cursorWidth = 1.0, this.textSpanDecorator, this.placeholderText = ' ', this.placeholderTextSpanDecorator, @@ -41,7 +43,8 @@ class _FlowyRichTextState extends State with Selectable { final _textKey = GlobalKey(); final _placeholderTextKey = GlobalKey(); - final lineHeight = 1.5; + final _lineHeight = 1.5; + double? _cursorHeight; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @@ -54,6 +57,13 @@ class _FlowyRichTextState extends State with Selectable { return _buildRichText(context); } + @override + void didUpdateWidget(covariant FlowyRichText oldWidget) { + super.didUpdateWidget(oldWidget); + + _cursorHeight = null; + } + @override Position start() => Position(path: widget.textNode.path, offset: 0); @@ -66,7 +76,7 @@ class _FlowyRichTextState extends State with Selectable { final textPosition = TextPosition(offset: position.offset); final cursorOffset = _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - final cursorHeight = widget.cursorHeight ?? + _cursorHeight ??= widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? 18.0; // default height @@ -74,7 +84,7 @@ class _FlowyRichTextState extends State with Selectable { cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - cursorHeight, + _cursorHeight!, ); } @@ -105,7 +115,7 @@ class _FlowyRichTextState extends State with Selectable { extentOffset: selection.end.offset, ); return _renderParagraph - .getBoxesForSelection(textSelection) + .getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max) .map((box) => box.toRect()) .toList(); } @@ -147,7 +157,7 @@ class _FlowyRichTextState extends State with Selectable { ? Colors.transparent : Colors.grey, fontSize: baseFontSize, - height: lineHeight, + height: _lineHeight, ), ), ], @@ -203,7 +213,7 @@ class _FlowyRichTextState extends State with Selectable { .map((insert) => RichTextStyle( attributes: insert.attributes ?? {}, text: insert.content, - height: lineHeight, + height: _lineHeight, ).toTextSpan()) .toList(growable: false), ); From fe2790fb684526ae121b9cacf2fd90c2375fc2d6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 11:49:54 +0800 Subject: [PATCH 065/224] fix: #811 The height of selection areas in same line is not same. --- .../lib/src/render/rich_text/flowy_rich_text.dart | 2 ++ .../lib/src/render/rich_text/rich_text_style.dart | 12 +----------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 01949aa99c..bdfae73d66 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -174,6 +174,8 @@ class _FlowyRichTextState extends State with Selectable { final textSpan = _textSpan; return RichText( key: _textKey, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, applyHeightToLastDescent: false), text: widget.textSpanDecorator != null ? widget.textSpanDecorator!(textSpan) : textSpan, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart index c924709948..4ac2adc39f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -192,17 +192,7 @@ class RichTextStyle { TextSpan toTextSpan() => _toTextSpan(height); double get topPadding { - if (height == 1.0) { - return 0; - } - // TODO: Need to be optimized. - final painter = - TextPainter(text: _toTextSpan(height), textDirection: TextDirection.ltr) - ..layout(); - final basePainter = - TextPainter(text: _toTextSpan(null), textDirection: TextDirection.ltr) - ..layout(); - return painter.height - basePainter.height; + return 0; } TextSpan _toTextSpan(double? height) { From aae2d96a4faf45934555af19d77e761c8dba2bfb Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 11 Aug 2022 13:04:45 +0800 Subject: [PATCH 066/224] chore: refactor multi-select type option and add GroupPB --- .../lib/plugins/board/application/group.dart | 20 ++ .../type_option/multi_select_type_option.dart | 29 ++- .../type_option/type_option_context.dart | 195 +++++++++++++++ .../widgets/header/type_option/builder.dart | 4 +- .../header/type_option/multi_select.dart | 4 +- frontend/rust-lib/Cargo.lock | 1 + .../src/entities/filter_entities/util.rs | 16 +- .../src/entities/group_entities/group.rs | 38 ++- .../src/entities/setting_entities.rs | 6 +- .../rust-lib/flowy-grid/src/event_handler.rs | 11 + frontend/rust-lib/flowy-grid/src/event_map.rs | 7 +- .../src/services/cell/cell_operation.rs | 4 +- .../src/services/field/field_builder.rs | 2 +- .../multi_select_type_option.rs | 18 +- .../selection_type_option/select_option.rs | 4 +- .../text_type_option/text_type_option.rs | 2 +- .../src/services/filter/filter_service.rs | 11 +- .../filter/impls/select_option_filter.rs | 4 +- .../flowy-grid/src/services/grid_editor.rs | 11 +- .../src/services/group/group_service.rs | 25 +- .../src/services/setting/setting_builder.rs | 15 +- .../flowy-grid/tests/grid/block_test/util.rs | 4 +- .../flowy-grid/tests/grid/cell_test/test.rs | 4 +- .../flowy-grid/tests/grid/grid_editor.rs | 2 +- .../flowy-grid/tests/grid/grid_test.rs | 6 +- shared-lib/Cargo.lock | 1 + shared-lib/flowy-grid-data-model/Cargo.toml | 1 + .../src/revision/filter_rev.rs | 3 - .../revision/{group_rev.rs => grid_group.rs} | 3 - .../src/revision/grid_setting_rev.rs | 226 +++++++++++------- .../src/revision/grid_sort.rs | 0 .../flowy-grid-data-model/src/revision/mod.rs | 6 +- .../src/client_folder/folder_pad.rs | 1 - .../src/client_grid/grid_revision_pad.rs | 122 +++++----- 34 files changed, 561 insertions(+), 245 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/group.dart create mode 100644 frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart rename shared-lib/flowy-grid-data-model/src/revision/{group_rev.rs => grid_group.rs} (72%) create mode 100644 shared-lib/flowy-grid-data-model/src/revision/grid_sort.rs diff --git a/frontend/app_flowy/lib/plugins/board/application/group.dart b/frontend/app_flowy/lib/plugins/board/application/group.dart new file mode 100644 index 0000000000..98f11b6b35 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/group.dart @@ -0,0 +1,20 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; + +class BoardGroupService { + final String gridId; + GridFieldPB? groupField; + + BoardGroupService(this.gridId); + + void setGroupField(GridFieldPB field) { + groupField = field; + } +} + +abstract class CanBeGroupField { + String get groupContent; +} + +// class SingleSelectGroup extends CanBeGroupField { +// final SingleSelectTypeOptionContext typeOptionContext; +// } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart index 1e99859a27..7a54c43c3d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/multi_select_type_option.dart @@ -4,22 +4,29 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'dart:async'; import 'select_option_type_option_bloc.dart'; import 'type_option_context.dart'; -import 'type_option_data_controller.dart'; import 'type_option_service.dart'; import 'package:protobuf/protobuf.dart'; -class MultiSelectTypeOptionContext - extends TypeOptionContext with ISelectOptionAction { +class MultiSelectAction with ISelectOptionAction { + final String gridId; + final String fieldId; final TypeOptionFFIService service; + final MultiSelectTypeOptionContext typeOptionContext; - MultiSelectTypeOptionContext({ - required MultiSelectTypeOptionWidgetDataParser dataParser, - required TypeOptionDataController dataController, - }) : service = TypeOptionFFIService( - gridId: dataController.gridId, - fieldId: dataController.field.id, - ), - super(dataParser: dataParser, dataController: dataController); + MultiSelectAction({ + required this.gridId, + required this.fieldId, + required this.typeOptionContext, + }) : service = TypeOptionFFIService( + gridId: gridId, + fieldId: fieldId, + ); + + MultiSelectTypeOptionPB get typeOption => typeOptionContext.typeOption; + + set typeOption(MultiSelectTypeOptionPB newTypeOption) { + typeOptionContext.typeOption = newTypeOption; + } @override List Function(SelectOptionPB) get deleteOption { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart new file mode 100644 index 0000000000..e3cd38f396 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart @@ -0,0 +1,195 @@ +import 'package:flowy_sdk/dispatch/dispatch.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/multi_select_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'type_option_data_controller.dart'; + +abstract class TypeOptionDataParser { + T fromBuffer(List buffer); +} + +// Number +typedef NumberTypeOptionContext = TypeOptionContext; + +class NumberTypeOptionWidgetDataParser + extends TypeOptionDataParser { + @override + NumberTypeOption fromBuffer(List buffer) { + return NumberTypeOption.fromBuffer(buffer); + } +} + +// RichText +typedef RichTextTypeOptionContext = TypeOptionContext; + +class RichTextTypeOptionWidgetDataParser + extends TypeOptionDataParser { + @override + RichTextTypeOption fromBuffer(List buffer) { + return RichTextTypeOption.fromBuffer(buffer); + } +} + +// Checkbox +typedef CheckboxTypeOptionContext = TypeOptionContext; + +class CheckboxTypeOptionWidgetDataParser + extends TypeOptionDataParser { + @override + CheckboxTypeOption fromBuffer(List buffer) { + return CheckboxTypeOption.fromBuffer(buffer); + } +} + +// URL +typedef URLTypeOptionContext = TypeOptionContext; + +class URLTypeOptionWidgetDataParser + extends TypeOptionDataParser { + @override + URLTypeOption fromBuffer(List buffer) { + return URLTypeOption.fromBuffer(buffer); + } +} + +// Date +typedef DateTypeOptionContext = TypeOptionContext; + +class DateTypeOptionDataParser extends TypeOptionDataParser { + @override + DateTypeOption fromBuffer(List buffer) { + return DateTypeOption.fromBuffer(buffer); + } +} + +// SingleSelect +typedef SingleSelectTypeOptionContext + = TypeOptionContext; + +class SingleSelectTypeOptionWidgetDataParser + extends TypeOptionDataParser { + @override + SingleSelectTypeOptionPB fromBuffer(List buffer) { + return SingleSelectTypeOptionPB.fromBuffer(buffer); + } +} + +// Multi-select +typedef MultiSelectTypeOptionContext + = TypeOptionContext; + +class MultiSelectTypeOptionWidgetDataParser + extends TypeOptionDataParser { + @override + MultiSelectTypeOptionPB fromBuffer(List buffer) { + return MultiSelectTypeOptionPB.fromBuffer(buffer); + } +} + +class TypeOptionContext { + T? _typeOptionObject; + final TypeOptionDataParser dataParser; + final TypeOptionDataController _dataController; + + TypeOptionContext({ + required this.dataParser, + required TypeOptionDataController dataController, + }) : _dataController = dataController; + + String get gridId => _dataController.gridId; + + String get fieldId => _dataController.field.id; + + Future loadTypeOptionData({ + required void Function(T) onCompleted, + required void Function(FlowyError) onError, + }) async { + await _dataController.loadTypeOptionData().then((result) { + result.fold((l) => null, (err) => onError(err)); + }); + + onCompleted(typeOption); + } + + T get typeOption { + if (_typeOptionObject != null) { + return _typeOptionObject!; + } + + final T object = _dataController.getTypeOption(dataParser); + _typeOptionObject = object; + return object; + } + + set typeOption(T typeOption) { + _dataController.typeOptionData = typeOption.writeToBuffer(); + _typeOptionObject = typeOption; + } +} + +abstract class TypeOptionFieldDelegate { + void onFieldChanged(void Function(String) callback); + void dispose(); +} + +abstract class IFieldTypeOptionLoader { + String get gridId; + Future> load(); + + Future> switchToField( + String fieldId, FieldType fieldType) { + final payload = EditFieldPayloadPB.create() + ..gridId = gridId + ..fieldId = fieldId + ..fieldType = fieldType; + + return GridEventSwitchToField(payload).send(); + } +} + +class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { + @override + final String gridId; + NewFieldTypeOptionLoader({ + required this.gridId, + }); + + @override + Future> load() { + final payload = CreateFieldPayloadPB.create() + ..gridId = gridId + ..fieldType = FieldType.RichText; + + return GridEventCreateFieldTypeOption(payload).send(); + } +} + +class FieldTypeOptionLoader extends IFieldTypeOptionLoader { + @override + final String gridId; + final GridFieldPB field; + + FieldTypeOptionLoader({ + required this.gridId, + required this.field, + }); + + @override + Future> load() { + final payload = GridFieldTypeOptionIdPB.create() + ..gridId = gridId + ..fieldId = field.id + ..fieldType = field.fieldType; + + return GridEventGetFieldTypeOption(payload).send(); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 8a6edcbf2c..40ce6fb82e 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -92,11 +92,11 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ ); case FieldType.MultiSelect: return MultiSelectTypeOptionWidgetBuilder( - makeTypeOptionContextWithDataController( + makeTypeOptionContextWithDataController( gridId: gridId, fieldType: fieldType, dataController: dataController, - ) as MultiSelectTypeOptionContext, + ), overlayDelegate, ); case FieldType.Number: diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart index e1b97fe087..9c0f860055 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart @@ -9,7 +9,7 @@ class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final MultiSelectTypeOptionWidget _widget; MultiSelectTypeOptionWidgetBuilder( - MultiSelectTypeOptionContext typeOptionContext, + MultiSelectAction typeOptionContext, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = MultiSelectTypeOptionWidget( typeOptionContext: typeOptionContext, @@ -21,7 +21,7 @@ class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { } class MultiSelectTypeOptionWidget extends TypeOptionWidget { - final MultiSelectTypeOptionContext typeOptionContext; + final MultiSelectAction typeOptionContext; final TypeOptionOverlayDelegate overlayDelegate; const MultiSelectTypeOptionWidget({ diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 643237bfe6..868e04a211 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -981,6 +981,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "tracing", ] [[package]] diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs index 03cc3d6111..b085f6bdd3 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs @@ -11,33 +11,33 @@ use std::convert::TryInto; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridFilter { +pub struct GridFilterConfiguration { #[pb(index = 1)] pub id: String, } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridFilterPB { +pub struct RepeatedGridConfigurationFilterPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From<&GridFilterRevision> for GridFilter { +impl std::convert::From<&GridFilterRevision> for GridFilterConfiguration { fn from(rev: &GridFilterRevision) -> Self { Self { id: rev.id.clone() } } } -impl std::convert::From>> for RepeatedGridFilterPB { +impl std::convert::From>> for RepeatedGridConfigurationFilterPB { fn from(revs: Vec>) -> Self { - RepeatedGridFilterPB { + RepeatedGridConfigurationFilterPB { items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), } } } -impl std::convert::From> for RepeatedGridFilterPB { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedGridConfigurationFilterPB { + fn from(items: Vec) -> Self { Self { items } } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index d05c2ad966..cc89a11e59 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -1,4 +1,4 @@ -use crate::entities::FieldType; +use crate::entities::{FieldType, GridRowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -8,7 +8,7 @@ use std::convert::TryInto; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridGroupPB { +pub struct GridGroupConfigurationPB { #[pb(index = 1)] pub id: String, @@ -19,9 +19,9 @@ pub struct GridGroupPB { pub sub_group_field_id: Option, } -impl std::convert::From<&GridGroupRevision> for GridGroupPB { +impl std::convert::From<&GridGroupRevision> for GridGroupConfigurationPB { fn from(rev: &GridGroupRevision) -> Self { - GridGroupPB { + GridGroupConfigurationPB { id: rev.id.clone(), group_field_id: rev.field_id.clone(), sub_group_field_id: rev.sub_field_id.clone(), @@ -29,21 +29,39 @@ impl std::convert::From<&GridGroupRevision> for GridGroupPB { } } -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +#[derive(ProtoBuf, Debug, Default, Clone)] pub struct RepeatedGridGroupPB { #[pb(index = 1)] - pub items: Vec, + groups: Vec, } -impl std::convert::From> for RepeatedGridGroupPB { - fn from(items: Vec) -> Self { +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct GroupPB { + #[pb(index = 1)] + pub group_id: String, + + #[pb(index = 2)] + pub desc: String, + + #[pb(index = 3)] + pub rows: Vec, +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct RepeatedGridGroupConfigurationPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::convert::From> for RepeatedGridGroupConfigurationPB { + fn from(items: Vec) -> Self { Self { items } } } -impl std::convert::From>> for RepeatedGridGroupPB { +impl std::convert::From>> for RepeatedGridGroupConfigurationPB { fn from(revs: Vec>) -> Self { - RepeatedGridGroupPB { + RepeatedGridGroupConfigurationPB { items: revs.iter().map(|rev| rev.as_ref().into()).collect(), } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 7828c144f0..4034e838aa 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -1,6 +1,6 @@ use crate::entities::{ CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, CreateGridSortPayloadPB, DeleteFilterPayloadPB, - DeleteGroupPayloadPB, RepeatedGridFilterPB, RepeatedGridGroupPB, RepeatedGridSortPB, + DeleteGroupPayloadPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RepeatedGridSortPB, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; @@ -22,10 +22,10 @@ pub struct GridSettingPB { pub current_layout_type: GridLayoutType, #[pb(index = 3)] - pub filters_by_field_id: HashMap, + pub filter_configuration_by_field_id: HashMap, #[pb(index = 4)] - pub groups_by_field_id: HashMap, + pub group_configuration_by_field_id: HashMap, #[pb(index = 5)] pub sorts_by_field_id: HashMap, diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index b0ef43f5ad..496ee5ed18 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -405,3 +405,14 @@ pub(crate) async fn update_date_cell_handler( let _ = editor.update_cell(params.into()).await?; Ok(()) } + +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn get_group_handler( + data: Data, + manager: AppData>, +) -> DataResult { + let params: GridIdPB = data.into_inner(); + let editor = manager.get_grid_editor(¶ms.value)?; + let group = editor.get_group().await?; + data_result(group) +} diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 0855ef0032..f9b0770174 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -37,7 +37,9 @@ pub fn create(grid_manager: Arc) -> Module { .event(GridEvent::GetSelectOptionCellData, get_select_option_handler) .event(GridEvent::UpdateSelectOptionCell, update_select_option_cell_handler) // Date - .event(GridEvent::UpdateDateCell, update_date_cell_handler); + .event(GridEvent::UpdateDateCell, update_date_cell_handler) + // Group + .event(GridEvent::GetGroup, update_date_cell_handler); module } @@ -204,4 +206,7 @@ pub enum GridEvent { /// will be used by the `update_cell` function. #[event(input = "DateChangesetPayloadPB")] UpdateDateCell = 80, + + #[event(input = "GridIdPB", output = "RepeatedGridGroupPB")] + GetGroup = 100, } diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index e34f3a8430..e73dbb8bc3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -61,7 +61,7 @@ pub fn apply_cell_data_changeset>( FieldType::SingleSelect => { SingleSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev) } - FieldType::MultiSelect => MultiSelectTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::MultiSelect => MultiSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::Checkbox => CheckboxTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::URL => URLTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), }?; @@ -109,7 +109,7 @@ pub fn try_decode_cell_data( .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::MultiSelect => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::Checkbox => field_rev .get_type_option_entry::(field_type)? diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs index de1f37b04f..6578ceb933 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs @@ -94,7 +94,7 @@ pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box NumberTypeOption::default().into(), FieldType::DateTime => DateTypeOption::default().into(), FieldType::SingleSelect => SingleSelectTypeOptionPB::default().into(), - FieldType::MultiSelect => MultiSelectTypeOption::default().into(), + FieldType::MultiSelect => MultiSelectTypeOptionPB::default().into(), FieldType::Checkbox => CheckboxTypeOption::default().into(), FieldType::URL => URLTypeOption::default().into(), }; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 187b81d060..a09c81e97f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -14,16 +14,16 @@ use serde::{Deserialize, Serialize}; // Multiple select #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct MultiSelectTypeOption { +pub struct MultiSelectTypeOptionPB { #[pb(index = 1)] pub options: Vec, #[pb(index = 2)] pub disable_color: bool, } -impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect); +impl_type_option!(MultiSelectTypeOptionPB, FieldType::MultiSelect); -impl SelectOptionOperation for MultiSelectTypeOption { +impl SelectOptionOperation for MultiSelectTypeOptionPB { fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellDataPB { let select_options = make_selected_select_options(cell_data, &self.options); SelectOptionCellDataPB { @@ -41,7 +41,7 @@ impl SelectOptionOperation for MultiSelectTypeOption { } } -impl CellDataOperation for MultiSelectTypeOption { +impl CellDataOperation for MultiSelectTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, @@ -93,9 +93,9 @@ impl CellDataOperation for MultiSele } #[derive(Default)] -pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOption); +pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOptionPB); impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOption); +impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOptionPB); impl MultiSelectTypeOptionBuilder { pub fn option(mut self, opt: SelectOptionPB) -> Self { self.0.options.push(opt); @@ -118,7 +118,7 @@ mod tests { use crate::services::cell::CellDataOperation; use crate::services::field::type_options::selection_type_option::*; use crate::services::field::FieldBuilder; - use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder}; + use crate::services::field::{MultiSelectTypeOptionBuilder, MultiSelectTypeOptionPB}; use flowy_grid_data_model::revision::FieldRevision; #[test] @@ -136,7 +136,7 @@ mod tests { .visibility(true) .build(); - let type_option = MultiSelectTypeOption::from(&field_rev); + let type_option = MultiSelectTypeOptionPB::from(&field_rev); let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR); let data = SelectOptionCellChangeset::from_insert(&option_ids).to_str(); @@ -170,7 +170,7 @@ mod tests { fn assert_multi_select_options( cell_data: String, - type_option: &MultiSelectTypeOption, + type_option: &MultiSelectTypeOptionPB, field_rev: &FieldRevision, expected: Vec, ) { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index d426844427..3e6c95c5ec 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -1,6 +1,6 @@ use crate::entities::{CellChangesetPB, FieldType, GridCellIdPB, GridCellIdParams}; use crate::services::cell::{CellBytes, CellBytesParser, CellData, CellDisplayable, FromCellChangeset, FromCellString}; -use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; +use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB}; use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::{internal_error, ErrorCode, FlowyResult}; @@ -130,7 +130,7 @@ pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult { - let type_option = MultiSelectTypeOption::from(field_rev); + let type_option = MultiSelectTypeOptionPB::from(field_rev); Ok(Box::new(type_option)) } ty => { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index 43e2b009d0..44ae81315d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -159,7 +159,7 @@ mod tests { .option(google_option.clone()) .option(facebook_option.clone()); let multi_select_field_rev = FieldBuilder::new(multi_select).build(); - let multi_type_option = MultiSelectTypeOption::from(&multi_select_field_rev); + let multi_type_option = MultiSelectTypeOptionPB::from(&multi_select_field_rev); let cell_data = multi_type_option .apply_changeset(cell_data_changeset.into(), None) .unwrap(); diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs index f3b1d72fb8..8ee5f6d99e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs @@ -3,7 +3,7 @@ use crate::entities::{FieldType, GridBlockChangesetPB}; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{AnyCellData, CellFilterOperation}; use crate::services::field::{ - CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption, + CheckboxTypeOption, DateTypeOption, MultiSelectTypeOptionPB, NumberTypeOption, RichTextTypeOption, SingleSelectTypeOptionPB, URLTypeOption, }; use crate::services::filter::filter_cache::{ @@ -22,8 +22,6 @@ use std::sync::Arc; use tokio::sync::RwLock; pub(crate) struct GridFilterService { - #[allow(dead_code)] - grid_id: String, scheduler: Arc, grid_pad: Arc>, block_manager: Arc, @@ -36,12 +34,10 @@ impl GridFilterService { block_manager: Arc, scheduler: S, ) -> Self { - let grid_id = grid_pad.read().await.grid_id(); let scheduler = Arc::new(scheduler); let filter_cache = FilterCache::from_grid_pad(&grid_pad).await; let filter_result_cache = FilterResultCache::new(); Self { - grid_id, grid_pad, block_manager, scheduler, @@ -134,8 +130,9 @@ impl GridFilterService { } async fn notify(&self, changesets: Vec) { + let grid_id = self.grid_pad.read().await.grid_id(); for changeset in changesets { - send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridBlock) + send_dart_notification(&grid_id, GridNotification::DidUpdateGridBlock) .payload(changeset) .send(); } @@ -217,7 +214,7 @@ fn filter_cell( FieldType::MultiSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs index e6cb9ff846..5ae8210746 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs @@ -2,7 +2,7 @@ use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; use crate::services::cell::{AnyCellData, CellFilterOperation}; -use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; +use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB}; use crate::services::field::{SelectOptionOperation, SelectedSelectOptions}; use flowy_error::FlowyResult; @@ -39,7 +39,7 @@ impl GridSelectOptionFilter { } } -impl CellFilterOperation for MultiSelectTypeOption { +impl CellFilterOperation for MultiSelectTypeOptionPB { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult { if !any_cell_data.is_multi_select() { return Ok(true); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index d98c9538d3..c50042edad 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -61,7 +61,8 @@ impl GridRevisionEditor { let block_manager = Arc::new(GridBlockManager::new(grid_id, &user, block_meta_revs, persistence).await?); let filter_service = Arc::new(GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await); - let group_service = Arc::new(GridGroupService::new()); + let group_service = + Arc::new(GridGroupService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await); let editor = Arc::new(Self { grid_id: grid_id.to_owned(), user, @@ -455,14 +456,14 @@ impl GridRevisionEditor { Ok(grid_setting) } - pub async fn get_grid_filter(&self, layout_type: &GridLayoutType) -> FlowyResult> { + pub async fn get_grid_filter(&self, layout_type: &GridLayoutType) -> FlowyResult> { let read_guard = self.grid_pad.read().await; let layout_rev = layout_type.clone().into(); match read_guard.get_filters(Some(&layout_rev), None) { Some(filter_revs) => Ok(filter_revs .iter() .map(|filter_rev| filter_rev.as_ref().into()) - .collect::>()), + .collect::>()), None => Ok(vec![]), } } @@ -561,6 +562,10 @@ impl GridRevisionEditor { }) } + pub async fn get_group(&self) -> FlowyResult { + todo!() + } + async fn modify(&self, f: F) -> FlowyResult<()> where F: for<'a> FnOnce(&'a mut GridRevisionPad) -> FlowyResult>, diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 255ec015d6..8bc8216cf9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,7 +1,26 @@ -pub struct GridGroupService {} +use crate::services::block_manager::GridBlockManager; +use crate::services::grid_editor_task::GridServiceTaskScheduler; +use flowy_sync::client_grid::GridRevisionPad; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub(crate) struct GridGroupService { + scheduler: Arc, + grid_pad: Arc>, + block_manager: Arc, +} impl GridGroupService { - pub fn new() -> Self { - Self {} + pub(crate) async fn new( + grid_pad: Arc>, + block_manager: Arc, + scheduler: S, + ) -> Self { + let scheduler = Arc::new(scheduler); + Self { + scheduler, + grid_pad, + block_manager, + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index be283981cf..03ed8c760b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -1,5 +1,6 @@ use crate::entities::{ - GridLayoutPB, GridLayoutType, GridSettingPB, RepeatedGridFilterPB, RepeatedGridGroupPB, RepeatedGridSortPB, + GridLayoutPB, GridLayoutType, GridSettingPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, + RepeatedGridSortPB, }; use flowy_grid_data_model::revision::{FieldRevision, GridSettingRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; @@ -43,21 +44,21 @@ impl GridSettingChangesetBuilder { pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[Arc]) -> GridSettingPB { let current_layout_type: GridLayoutType = grid_setting_rev.layout.clone().into(); let filters_by_field_id = grid_setting_rev - .get_all_filter(field_revs) + .get_all_filters(field_revs) .map(|filters_by_field_id| { filters_by_field_id .into_iter() .map(|(k, v)| (k, v.into())) - .collect::>() + .collect::>() }) .unwrap_or_default(); let groups_by_field_id = grid_setting_rev - .get_all_group() + .get_all_groups(field_revs) .map(|groups_by_field_id| { groups_by_field_id .into_iter() .map(|(k, v)| (k, v.into())) - .collect::>() + .collect::>() }) .unwrap_or_default(); let sorts_by_field_id = grid_setting_rev @@ -73,8 +74,8 @@ pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[A GridSettingPB { layouts: GridLayoutPB::all(), current_layout_type, - filters_by_field_id, - groups_by_field_id, + filter_configuration_by_field_id: filters_by_field_id, + group_configuration_by_field_id: groups_by_field_id, sorts_by_field_id, } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index d2a430f591..ddbf5c30fe 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -2,7 +2,7 @@ use flowy_grid::entities::FieldType; use std::sync::Arc; use flowy_grid::services::field::{ - DateCellChangesetPB, MultiSelectTypeOption, SelectOptionPB, SingleSelectTypeOptionPB, SELECTION_IDS_SEPARATOR, + DateCellChangesetPB, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB, SELECTION_IDS_SEPARATOR, }; use flowy_grid::services::row::RowRevisionBuilder; use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; @@ -87,7 +87,7 @@ impl<'a> GridRowTestBuilder<'a> { F: Fn(Vec) -> Vec, { let multi_select_field = self.field_rev_with_type(&FieldType::MultiSelect); - let type_option = MultiSelectTypeOption::from(&multi_select_field); + let type_option = MultiSelectTypeOptionPB::from(&multi_select_field); let options = f(type_option.options); let ops_ids = options .iter() diff --git a/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs index e8435c2d00..ccc779d17a 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs @@ -3,7 +3,7 @@ use crate::grid::cell_test::script::GridCellTest; use crate::grid::field_test::util::make_date_cell_string; use flowy_grid::entities::{CellChangesetPB, FieldType}; use flowy_grid::services::field::selection_type_option::SelectOptionCellChangeset; -use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; +use flowy_grid::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB}; #[tokio::test] async fn grid_cell_update() { @@ -28,7 +28,7 @@ async fn grid_cell_update() { SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() } FieldType::MultiSelect => { - let type_option = MultiSelectTypeOption::from(field_rev); + let type_option = MultiSelectTypeOptionPB::from(field_rev); SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() } FieldType::Checkbox => "1".to_string(), diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index 09f52ddb15..f4a1528f8f 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -75,7 +75,7 @@ impl GridEditorTest { .row_revs } - pub async fn grid_filters(&self) -> Vec { + pub async fn grid_filters(&self) -> Vec { let layout_type = GridLayoutType::Table; self.editor.get_grid_filter(&layout_type).await.unwrap() } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs index 63bea4cc3d..11866b7d1c 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs @@ -2,7 +2,7 @@ use crate::grid::script::EditorScript::*; use crate::grid::script::*; use chrono::NaiveDateTime; use flowy_grid::services::field::{ - DateCellContentChangeset, DateCellData, MultiSelectTypeOption, SelectOption, SelectOptionCellContentChangeset, + DateCellContentChangeset, DateCellData, MultiSelectTypeOptionPB, SelectOption, SelectOptionCellContentChangeset, SingleSelectTypeOption, SELECTION_IDS_SEPARATOR, }; use flowy_grid::services::row::{decode_cell_data_from_type_option_cell_data, CreateRowMetaBuilder}; @@ -250,7 +250,7 @@ async fn grid_row_add_cells_test() { builder.add_select_option_cell(&field.id, option.id.clone()).unwrap(); } FieldType::MultiSelect => { - let type_option = MultiSelectTypeOption::from(field); + let type_option = MultiSelectTypeOptionPB::from(field); let ops_ids = type_option .options .iter() @@ -327,7 +327,7 @@ async fn grid_cell_update() { SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() } FieldType::MultiSelect => { - let type_option = MultiSelectTypeOption::from(field_meta); + let type_option = MultiSelectTypeOptionPB::from(field_meta); SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() } FieldType::Checkbox => "1".to_string(), diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 9b8e5f6081..da8bc0ff1c 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -436,6 +436,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "tracing", ] [[package]] diff --git a/shared-lib/flowy-grid-data-model/Cargo.toml b/shared-lib/flowy-grid-data-model/Cargo.toml index b0162c82a8..3e640ec1af 100644 --- a/shared-lib/flowy-grid-data-model/Cargo.toml +++ b/shared-lib/flowy-grid-data-model/Cargo.toml @@ -13,6 +13,7 @@ serde_repr = "0.1" nanoid = "0.4.0" flowy-error-code = { path = "../flowy-error-code"} indexmap = {version = "1.8.1", features = ["serde"]} +tracing = { version = "0.1", features = ["log"] } [build-dependencies] lib-infra = { path = "../lib-infra", features = ["protobuf_file_gen"] } diff --git a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs index abf129c415..06e7971cf0 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs @@ -1,7 +1,4 @@ -use crate::revision::FieldTypeRevision; -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use std::sync::Arc; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] pub struct GridFilterRevision { diff --git a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs similarity index 72% rename from shared-lib/flowy-grid-data-model/src/revision/group_rev.rs rename to shared-lib/flowy-grid-data-model/src/revision/grid_group.rs index e856354f6c..268682d552 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/group_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs @@ -1,7 +1,4 @@ -use crate::revision::FieldTypeRevision; -use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use std::sync::Arc; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct GridGroupRevision { diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 095b0dcd97..153ee10961 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -1,5 +1,5 @@ use crate::revision::filter_rev::GridFilterRevision; -use crate::revision::group_rev::GridGroupRevision; +use crate::revision::grid_group::GridGroupRevision; use crate::revision::{FieldRevision, FieldTypeRevision}; use indexmap::IndexMap; use nanoid::nanoid; @@ -21,28 +21,28 @@ pub fn gen_grid_sort_id() -> String { nanoid!(6) } +pub type GridFilters = SettingContainer; +pub type GridFilterRevisionMap = GridObjectRevisionMap; +pub type FiltersByFieldId = HashMap>>; +// +pub type GridGroups = SettingContainer; +pub type GridGroupRevisionMap = GridObjectRevisionMap; +pub type GroupsByFieldId = HashMap>>; +// +pub type GridSorts = SettingContainer; +pub type GridSortRevisionMap = GridObjectRevisionMap; +pub type SortsByFieldId = HashMap>>; + #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] pub struct GridSettingRevision { pub layout: GridLayoutRevision, - /// Each layout contains multiple key/value. - /// Key: field_id - /// Value: this value contains key/value. - /// Key: FieldType, - /// Value: the corresponding filters. - #[serde(with = "indexmap::serde_seq")] - filters: IndexMap>, + pub filters: GridFilters, - /// Each layout contains multiple key/value. - /// Key: field_id - /// Value: this value contains key/value. - /// Key: FieldType, - /// Value: the corresponding groups. - #[serde(skip, with = "indexmap::serde_seq")] - pub groups: IndexMap>, + pub groups: GridGroups, - #[serde(skip, with = "indexmap::serde_seq")] - pub sorts: IndexMap>, + #[serde(skip)] + pub sorts: GridSorts, } #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] @@ -65,12 +65,18 @@ impl std::default::Default for GridLayoutRevision { } } -pub type FiltersByFieldId = HashMap>>; -pub type GroupsByFieldId = HashMap>>; -pub type SortsByFieldId = HashMap>>; impl GridSettingRevision { - pub fn get_all_group(&self) -> Option { - None + pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { + self.groups.get_all_objects(&self.layout, field_revs) + } + + pub fn get_groups( + &self, + layout: &GridLayoutRevision, + field_id: &str, + field_type_rev: &FieldTypeRevision, + ) -> Option>> { + self.groups.get_objects(layout, field_id, field_type_rev) } pub fn get_mut_groups( @@ -79,10 +85,7 @@ impl GridSettingRevision { field_id: &str, field_type: &FieldTypeRevision, ) -> Option<&mut Vec>> { - self.groups - .get_mut(layout) - .and_then(|group_rev_map_by_field_id| group_rev_map_by_field_id.get_mut(field_id)) - .and_then(|group_rev_map| group_rev_map.get_mut(field_type)) + self.groups.get_mut_objects(layout, field_id, field_type) } pub fn insert_group( @@ -90,59 +93,13 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, - filter_rev: GridGroupRevision, + group_rev: GridGroupRevision, ) { - let filter_rev_map_by_field_id = self.groups.entry(layout.clone()).or_insert_with(IndexMap::new); - let filter_rev_map = filter_rev_map_by_field_id - .entry(field_id.to_string()) - .or_insert_with(GridGroupRevisionMap::new); - - filter_rev_map - .entry(field_type.to_owned()) - .or_insert_with(Vec::new) - .push(Arc::new(filter_rev)) + self.groups.insert_object(layout, field_id, field_type, group_rev); } - pub fn get_all_sort(&self) -> Option { - None - } - - /// Return the Filters of the current layout - pub fn get_all_filter(&self, field_revs: &[Arc]) -> Option { - let layout = &self.layout; - // Acquire the read lock of the filters. - let filter_rev_map_by_field_id = self.filters.get(layout)?; - // Get the filters according to the FieldType, so we need iterate the field_revs. - let filters_by_field_id = field_revs - .iter() - .flat_map(|field_rev| { - let field_type = &field_rev.field_type_rev; - let field_id = &field_rev.id; - - let filter_rev_map: &GridFilterRevisionMap = filter_rev_map_by_field_id.get(field_id)?; - let filters: Vec> = filter_rev_map.get(field_type)?.clone(); - Some((field_rev.id.clone(), filters)) - }) - .collect::(); - Some(filters_by_field_id) - } - - #[allow(dead_code)] - fn get_filter_rev_map(&self, layout: &GridLayoutRevision, field_id: &str) -> Option<&GridFilterRevisionMap> { - let filter_rev_map_by_field_id = self.filters.get(layout)?; - filter_rev_map_by_field_id.get(field_id) - } - - pub fn get_mut_filters( - &mut self, - layout: &GridLayoutRevision, - field_id: &str, - field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { - self.filters - .get_mut(layout) - .and_then(|filter_rev_map_by_field_id| filter_rev_map_by_field_id.get_mut(field_id)) - .and_then(|filter_rev_map| filter_rev_map.get_mut(field_type)) + pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { + self.filters.get_all_objects(&self.layout, field_revs) } pub fn get_filters( @@ -151,11 +108,16 @@ impl GridSettingRevision { field_id: &str, field_type_rev: &FieldTypeRevision, ) -> Option>> { - self.filters - .get(layout) - .and_then(|filter_rev_map_by_field_id| filter_rev_map_by_field_id.get(field_id)) - .and_then(|filter_rev_map| filter_rev_map.get(field_type_rev)) - .cloned() + self.filters.get_objects(layout, field_id, field_type_rev) + } + + pub fn get_mut_filters( + &mut self, + layout: &GridLayoutRevision, + field_id: &str, + field_type: &FieldTypeRevision, + ) -> Option<&mut Vec>> { + self.filters.get_mut_objects(layout, field_id, field_type) } pub fn insert_filter( @@ -165,15 +127,11 @@ impl GridSettingRevision { field_type: &FieldTypeRevision, filter_rev: GridFilterRevision, ) { - let filter_rev_map_by_field_id = self.filters.entry(layout.clone()).or_insert_with(IndexMap::new); - let filter_rev_map = filter_rev_map_by_field_id - .entry(field_id.to_string()) - .or_insert_with(GridFilterRevisionMap::new); + self.filters.insert_object(layout, field_id, field_type, filter_rev); + } - filter_rev_map - .entry(field_type.to_owned()) - .or_insert_with(Vec::new) - .push(Arc::new(filter_rev)) + pub fn get_all_sort(&self) -> Option { + None } } @@ -183,8 +141,94 @@ pub struct GridSortRevision { pub field_id: Option, } -pub type GridFilterRevisionMap = GridObjectRevisionMap; -pub type GridGroupRevisionMap = GridObjectRevisionMap; +#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] +#[serde(transparent)] +pub struct SettingContainer +where + T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, +{ + /// Each layout contains multiple key/value. + /// Key: field_id + /// Value: this value contains key/value. + /// Key: FieldType, + /// Value: the corresponding objects. + #[serde(with = "indexmap::serde_seq")] + inner: IndexMap>>, +} + +impl SettingContainer +where + T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, +{ + pub fn get_mut_objects( + &mut self, + layout: &GridLayoutRevision, + field_id: &str, + field_type: &FieldTypeRevision, + ) -> Option<&mut Vec>> { + let value = self + .inner + .get_mut(layout) + .and_then(|object_rev_map_by_field_id| object_rev_map_by_field_id.get_mut(field_id)) + .and_then(|object_rev_map| object_rev_map.get_mut(field_type)); + if value.is_none() { + tracing::warn!("Can't find the {:?} with", std::any::type_name::()); + } + value + } + pub fn get_objects( + &self, + layout: &GridLayoutRevision, + field_id: &str, + field_type_rev: &FieldTypeRevision, + ) -> Option>> { + self.inner + .get(layout) + .and_then(|object_rev_map_by_field_id| object_rev_map_by_field_id.get(field_id)) + .and_then(|object_rev_map| object_rev_map.get(field_type_rev)) + .cloned() + } + + pub fn get_all_objects( + &self, + layout: &GridLayoutRevision, + field_revs: &[Arc], + ) -> Option>>> { + // Acquire the read lock. + let object_rev_map_by_field_id = self.inner.get(layout)?; + // Get the objects according to the FieldType, so we need iterate the field_revs. + let objects_by_field_id = field_revs + .iter() + .flat_map(|field_rev| { + let field_type = &field_rev.field_type_rev; + let field_id = &field_rev.id; + + let object_rev_map = object_rev_map_by_field_id.get(field_id)?; + let objects: Vec> = object_rev_map.get(field_type)?.clone(); + Some((field_rev.id.clone(), objects)) + }) + .collect::>>>(); + Some(objects_by_field_id) + } + + pub fn insert_object( + &mut self, + layout: &GridLayoutRevision, + field_id: &str, + field_type: &FieldTypeRevision, + object: T, + ) { + let object_rev_map_by_field_id = self.inner.entry(layout.clone()).or_insert_with(IndexMap::new); + let object_rev_map = object_rev_map_by_field_id + .entry(field_id.to_string()) + .or_insert_with(GridObjectRevisionMap::::new); + + object_rev_map + .entry(field_type.to_owned()) + .or_insert_with(Vec::new) + .push(Arc::new(object)) + } +} #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] #[serde(transparent)] diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_sort.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_sort.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shared-lib/flowy-grid-data-model/src/revision/mod.rs b/shared-lib/flowy-grid-data-model/src/revision/mod.rs index 9f4b34300e..9736442557 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/mod.rs @@ -1,9 +1,11 @@ mod filter_rev; +mod grid_group; mod grid_rev; mod grid_setting_rev; -mod group_rev; +mod grid_sort; pub use filter_rev::*; +pub use grid_group::*; pub use grid_rev::*; pub use grid_setting_rev::*; -pub use group_rev::*; +pub use grid_sort::*; diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index 28c8e087a1..63df0ce828 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -457,7 +457,6 @@ mod tests { AppRevision, FolderRevision, TrashRevision, ViewRevision, WorkspaceRevision, }; use lib_ot::core::{OperationTransform, TextDelta, TextDeltaBuilder}; - use serde_json::json; #[test] fn folder_add_workspace() { diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index 0d793aff47..73c32834b1 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -1,4 +1,6 @@ -use crate::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; +use crate::entities::grid::{ + CreateGridFilterParams, CreateGridGroupParams, FieldChangesetParams, GridSettingChangesetParams, +}; use crate::entities::revision::{md5, RepeatedRevision, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_delta_from_revisions}; @@ -380,88 +382,65 @@ impl GridRevisionPad { self.modify_grid(|grid_rev| { let mut is_changed = None; let layout_rev = changeset.layout_type; - if let Some(params) = changeset.insert_filter { - let filter_rev = GridFilterRevision { - id: gen_grid_filter_id(), - field_id: params.field_id.clone(), - condition: params.condition, - content: params.content, - }; - - grid_rev - .setting - .insert_filter(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev, filter_rev); + grid_rev.setting.insert_filter( + &layout_rev, + ¶ms.field_id, + ¶ms.field_type_rev, + make_filter_revision(¶ms), + ); is_changed = Some(()) } if let Some(params) = changeset.delete_filter { - match grid_rev - .setting - .get_mut_filters(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) + if let Some(filters) = + grid_rev + .setting + .get_mut_filters(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) { - Some(filters) => { - filters.retain(|filter| filter.id != params.filter_id); - } - None => { - tracing::warn!("Can't find the filter with {:?}", layout_rev); - } + filters.retain(|filter| filter.id != params.filter_id); } } if let Some(params) = changeset.insert_group { - let group_rev = GridGroupRevision { - id: gen_grid_group_id(), - field_id: params.field_id.clone(), - sub_field_id: params.sub_field_id, - }; - - grid_rev - .setting - .insert_group(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev, group_rev); + grid_rev.setting.insert_group( + &layout_rev, + ¶ms.field_id, + ¶ms.field_type_rev, + make_group_revision(¶ms), + ); is_changed = Some(()); } if let Some(params) = changeset.delete_group { - // match grid_rev.setting.groups.get_mut(&layout_rev) { - // Some(groups) => groups.retain(|group| group.id != delete_group_id), - // None => { - // tracing::warn!("Can't find the group with {:?}", layout_rev); - // } - // } - - match grid_rev - .setting - .get_mut_groups(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) + if let Some(groups) = + grid_rev + .setting + .get_mut_groups(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) { - Some(groups) => { - groups.retain(|filter| filter.id != params.group_id); - } - None => { - tracing::warn!("Can't find the group with {:?}", layout_rev); - } + groups.retain(|filter| filter.id != params.group_id); } } if let Some(sort) = changeset.insert_sort { - let rev = GridSortRevision { - id: gen_grid_sort_id(), - field_id: sort.field_id, - }; - - grid_rev - .setting - .sorts - .entry(layout_rev.clone()) - .or_insert_with(std::vec::Vec::new) - .push(rev); + // let rev = GridSortRevision { + // id: gen_grid_sort_id(), + // field_id: sort.field_id, + // }; + // + // grid_rev + // .setting + // .sorts + // .entry(layout_rev.clone()) + // .or_insert_with(std::vec::Vec::new) + // .push(rev); is_changed = Some(()) } if let Some(delete_sort_id) = changeset.delete_sort { - match grid_rev.setting.sorts.get_mut(&layout_rev) { - Some(sorts) => sorts.retain(|sort| sort.id != delete_sort_id), - None => { - tracing::warn!("Can't find the sort with {:?}", layout_rev); - } - } + // match grid_rev.setting.sorts.get_mut(&layout_rev) { + // Some(sorts) => sorts.retain(|sort| sort.id != delete_sort_id), + // None => { + // tracing::warn!("Can't find the sort with {:?}", layout_rev); + // } + // } } Ok(is_changed) }) @@ -579,3 +558,20 @@ impl std::default::Default for GridRevisionPad { } } } + +fn make_filter_revision(params: &CreateGridFilterParams) -> GridFilterRevision { + GridFilterRevision { + id: gen_grid_filter_id(), + field_id: params.field_id.clone(), + condition: params.condition.clone(), + content: params.content.clone(), + } +} + +fn make_group_revision(params: &CreateGridGroupParams) -> GridGroupRevision { + GridGroupRevision { + id: gen_grid_group_id(), + field_id: params.field_id.clone(), + sub_field_id: params.sub_field_id.clone(), + } +} From 08b9930510f98832abe276aa8d5fb7a7dc51e214 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 11 Aug 2022 13:25:55 +0800 Subject: [PATCH 067/224] refactor: prefer namespace isolation, remove Grid keyword in some structs --- .../plugins/board/application/board_bloc.dart | 22 +-- .../application/board_data_controller.dart | 126 ++++++++++++++++++ .../lib/plugins/board/application/group.dart | 4 +- .../board/presentation/board_page.dart | 2 +- .../lib/plugins/board/presentation/card.dart | 2 +- .../grid/application/block/block_cache.dart | 6 +- .../cell_service/cell_field_notifier.dart | 2 +- .../cell/cell_service/cell_service.dart | 2 +- .../cell/cell_service/context_builder.dart | 6 +- .../grid/application/cell/date_cell_bloc.dart | 17 ++- .../field/field_action_sheet_bloc.dart | 14 +- .../grid/application/field/field_cache.dart | 34 ++--- .../application/field/field_cell_bloc.dart | 4 +- .../application/field/field_editor_bloc.dart | 6 +- .../application/field/field_listener.dart | 11 +- .../grid/application/field/field_service.dart | 6 +- .../field/field_type_option_edit_bloc.dart | 6 +- .../grid/application/field/grid_listener.dart | 11 +- .../type_option/type_option_context.dart | 4 +- .../type_option_data_controller.dart | 12 +- .../plugins/grid/application/grid_bloc.dart | 16 +-- .../application/grid_data_controller.dart | 12 +- .../grid/application/grid_header_bloc.dart | 14 +- .../grid/application/grid_service.dart | 9 +- .../lib/plugins/grid/application/prelude.dart | 4 +- .../row/row_action_sheet_bloc.dart | 7 +- .../grid/application/row/row_bloc.dart | 14 +- .../grid/application/row/row_cache.dart | 75 +++++------ .../application/row/row_data_controller.dart | 4 +- .../grid/application/row/row_listener.dart | 10 +- .../grid/application/row/row_service.dart | 8 +- .../application/setting/property_bloc.dart | 10 +- .../plugins/grid/presentation/grid_page.dart | 6 +- .../grid/presentation/layout/layout.dart | 10 +- .../widgets/header/field_cell.dart | 2 +- .../header/field_type_option_editor.dart | 4 +- .../widgets/header/grid_header.dart | 2 +- .../widgets/header/type_option/builder.dart | 2 +- .../presentation/widgets/row/grid_row.dart | 2 +- .../widgets/row/row_action_sheet.dart | 2 +- .../widgets/toolbar/grid_property.dart | 2 +- .../widgets/emoji_picker/src/emoji_lists.dart | 2 +- .../flowy-grid/src/entities/block_entities.rs | 56 ++++---- .../flowy-grid/src/entities/field_entities.rs | 110 +++++++-------- .../flowy-grid/src/entities/grid_entities.rs | 6 +- .../src/entities/group_entities/group.rs | 4 +- .../flowy-grid/src/entities/row_entities.rs | 10 +- .../rust-lib/flowy-grid/src/event_handler.rs | 28 ++-- frontend/rust-lib/flowy-grid/src/event_map.rs | 22 +-- .../flowy-grid/src/services/block_manager.rs | 13 +- .../src/services/block_revision_editor.rs | 10 +- .../src/services/field/field_builder.rs | 4 +- .../flowy-grid/src/services/grid_editor.rs | 32 ++--- .../flowy-grid/src/services/row/row_loader.rs | 26 ++-- .../tests/grid/block_test/script.rs | 4 +- .../flowy-grid/tests/grid/field_test/util.rs | 4 +- .../flowy-grid/tests/grid/grid_editor.rs | 2 +- 57 files changed, 493 insertions(+), 352 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 5bf941ef99..5c2072d2f1 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -56,7 +56,7 @@ class BoardBloc extends Bloc { didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - groupByField: (GridFieldPB field) { + groupByField: (FieldPB field) { emit(state.copyWith(groupField: Some(field))); }, ); @@ -95,12 +95,12 @@ class BoardBloc extends Bloc { ); } - void _buildColumnItems(List rowInfos) { + void _buildColumnItems(List rowInfos) { for (final rowInfo in rowInfos) {} } - void _buildColumns(UnmodifiableListView fields) { - GridFieldPB? groupField; + void _buildColumns(UnmodifiableListView fields) { + FieldPB? groupField; for (final field in fields) { if (field.fieldType == FieldType.SingleSelect) { groupField = field; @@ -112,7 +112,7 @@ class BoardBloc extends Bloc { add(BoardEvent.groupByField(groupField!)); } - void _buildColumnsFromSingleSelect(GridFieldPB field) { + void _buildColumnsFromSingleSelect(FieldPB field) { final typeOptionContext = makeTypeOptionContext( gridId: _gridDataController.gridId, field: field, @@ -151,7 +151,7 @@ class BoardBloc extends Bloc { class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = InitialGrid; const factory BoardEvent.createRow() = _CreateRow; - const factory BoardEvent.groupByField(GridFieldPB field) = _GroupByField; + const factory BoardEvent.groupByField(FieldPB field) = _GroupByField; const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; @@ -162,8 +162,8 @@ class BoardState with _$BoardState { const factory BoardState({ required String gridId, required Option grid, - required Option groupField, - required List rowInfos, + required Option groupField, + required List rowInfos, required GridLoadingState loadingState, }) = _BoardState; @@ -184,9 +184,9 @@ class GridLoadingState with _$GridLoadingState { } class GridFieldEquatable extends Equatable { - final UnmodifiableListView _fields; + final UnmodifiableListView _fields; const GridFieldEquatable( - UnmodifiableListView fields, + UnmodifiableListView fields, ) : _fields = fields; @override @@ -203,7 +203,7 @@ class GridFieldEquatable extends Equatable { ]; } - UnmodifiableListView get value => UnmodifiableListView(_fields); + UnmodifiableListView get value => UnmodifiableListView(_fields); } class TextItem extends ColumnItem { diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart new file mode 100644 index 0000000000..d52b516663 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -0,0 +1,126 @@ +// import 'dart:collection'; + +// import 'package:flowy_sdk/log.dart'; +// import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +// import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +// import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +// import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +// import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; +// import 'dart:async'; +// import 'package:dartz/dartz.dart'; + +// typedef OnFieldsChanged = void Function(UnmodifiableListView); +// typedef OnGridChanged = void Function(GridPB); + +// typedef OnRowsChanged = void Function( +// List rowInfos, +// GridRowChangeReason, +// ); +// typedef ListenONRowChangedCondition = bool Function(); + +// class GridDataController { +// final String gridId; +// final GridService _gridFFIService; +// final GridFieldCache fieldCache; + +// // key: the block id +// final LinkedHashMap _blocks; +// UnmodifiableMapView get blocks => +// UnmodifiableMapView(_blocks); + +// OnRowsChanged? _onRowChanged; +// OnFieldsChanged? _onFieldsChanged; +// OnGridChanged? _onGridChanged; + +// List get rowInfos { +// final List rows = []; +// for (var block in _blocks.values) { +// rows.addAll(block.rows); +// } +// return rows; +// } + +// GridDataController({required ViewPB view}) +// : gridId = view.id, +// _blocks = LinkedHashMap.identity(), +// _gridFFIService = GridService(gridId: view.id), +// fieldCache = GridFieldCache(gridId: view.id); + +// void addListener({ +// required OnGridChanged onGridChanged, +// required OnRowsChanged onRowsChanged, +// required OnFieldsChanged onFieldsChanged, +// }) { +// _onGridChanged = onGridChanged; +// _onRowChanged = onRowsChanged; +// _onFieldsChanged = onFieldsChanged; + +// fieldCache.addListener(onFields: (fields) { +// _onFieldsChanged?.call(UnmodifiableListView(fields)); +// }); +// } + +// Future> loadData() async { +// final result = await _gridFFIService.loadGrid(); +// return Future( +// () => result.fold( +// (grid) async { +// _initialBlocks(grid.blocks); +// _onGridChanged?.call(grid); +// return await _loadFields(grid); +// }, +// (err) => right(err), +// ), +// ); +// } + +// void createRow() { +// _gridFFIService.createRow(); +// } + +// Future dispose() async { +// await _gridFFIService.closeGrid(); +// await fieldCache.dispose(); + +// for (final blockCache in _blocks.values) { +// blockCache.dispose(); +// } +// } + +// void _initialBlocks(List blocks) { +// for (final block in blocks) { +// if (_blocks[block.id] != null) { +// Log.warn("Initial duplicate block's cache: ${block.id}"); +// return; +// } + +// final cache = GridBlockCache( +// gridId: gridId, +// block: block, +// fieldCache: fieldCache, +// ); + +// cache.addListener( +// onChangeReason: (reason) { +// _onRowChanged?.call(rowInfos, reason); +// }, +// ); + +// _blocks[block.id] = cache; +// } +// } + +// Future> _loadFields(GridPB grid) async { +// final result = await _gridFFIService.getFields(fieldIds: grid.fields); +// return Future( +// () => result.fold( +// (fields) { +// fieldCache.fields = fields.items; +// _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields)); +// return left(unit); +// }, +// (err) => right(err), +// ), +// ); +// } +// } diff --git a/frontend/app_flowy/lib/plugins/board/application/group.dart b/frontend/app_flowy/lib/plugins/board/application/group.dart index 98f11b6b35..1e59350826 100644 --- a/frontend/app_flowy/lib/plugins/board/application/group.dart +++ b/frontend/app_flowy/lib/plugins/board/application/group.dart @@ -2,11 +2,11 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; class BoardGroupService { final String gridId; - GridFieldPB? groupField; + FieldPB? groupField; BoardGroupService(this.gridId); - void setGroupField(GridFieldPB field) { + void setGroupField(FieldPB field) { groupField = field; } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 1155168e70..60b788ae6d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -88,7 +88,7 @@ class BoardContent extends StatelessWidget { } Widget _buildCard(BuildContext context, ColumnItem item) { - final rowInfo = item as GridRowInfo; + final rowInfo = item as RowInfo; return AppFlowyColumnItemCard( key: ObjectKey(item), child: BoardCard(rowInfo: rowInfo), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card.dart index b98739eea8..0511b96b03 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card.dart @@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:flutter/material.dart'; class BoardCard extends StatelessWidget { - final GridRowInfo rowInfo; + final RowInfo rowInfo; const BoardCard({required this.rowInfo, Key? key}) : super(key: key); diff --git a/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart index 5569311228..ecfea7e119 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart @@ -9,11 +9,11 @@ import 'block_listener.dart'; /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information class GridBlockCache { final String gridId; - final GridBlockPB block; + final BlockPB block; late GridRowCache _rowCache; late GridBlockListener _listener; - List get rows => _rowCache.rows; + List get rows => _rowCache.rows; GridRowCache get rowCache => _rowCache; GridBlockCache({ @@ -42,7 +42,7 @@ class GridBlockCache { } void addListener({ - required void Function(GridRowChangeReason) onChangeReason, + required void Function(RowChangeReason) onChangeReason, bool Function()? listenWhen, }) { _rowCache.onRowsChanged((reason) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart index 1b2393671c..d4cca373bc 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'cell_service.dart'; abstract class IGridCellFieldNotifier { - void onCellFieldChanged(void Function(GridFieldPB) callback); + void onCellFieldChanged(void Function(FieldPB) callback); void onCellDispose(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart index 7dc15a01ee..bb750b4b89 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart @@ -60,7 +60,7 @@ class GridCellIdentifier with _$GridCellIdentifier { const factory GridCellIdentifier({ required String gridId, required String rowId, - required GridFieldPB field, + required FieldPB field, }) = _GridCellIdentifier; // ignore: unused_element diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 09564b46b0..b7c68a7937 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -166,7 +166,7 @@ class IGridCellController extends Equatable { String get fieldId => cellId.field.id; - GridFieldPB get field => cellId.field; + FieldPB get field => cellId.field; FieldType get fieldType => cellId.field.fieldType; @@ -321,8 +321,8 @@ class GridCellFieldNotifierImpl extends IGridCellFieldNotifier { } @override - void onCellFieldChanged(void Function(GridFieldPB p1) callback) { - _onChangesetFn = (GridFieldChangesetPB changeset) { + void onCellFieldChanged(void Function(FieldPB p1) callback) { + _onChangesetFn = (FieldChangesetPB changeset) { for (final updatedField in changeset.updatedFields) { callback(updatedField); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart index c4f79f1f90..fdc0ad1082 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart @@ -10,15 +10,18 @@ class DateCellBloc extends Bloc { final GridDateCellController cellContext; void Function()? _onCellChangedFn; - DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) { + DateCellBloc({required this.cellContext}) + : super(DateCellState.initial(cellContext)) { on( (event, emit) async { event.when( initial: () => _startListening(), didReceiveCellUpdate: (DateCellDataPB? cellData) { - emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData))); + emit(state.copyWith( + data: cellData, dateStr: _dateStrFromCellData(cellData))); }, - didReceiveFieldUpdate: (GridFieldPB value) => emit(state.copyWith(field: value)), + didReceiveFieldUpdate: (FieldPB value) => + emit(state.copyWith(field: value)), ); }, ); @@ -48,8 +51,10 @@ class DateCellBloc extends Bloc { @freezed class DateCellEvent with _$DateCellEvent { const factory DateCellEvent.initial() = _InitialCell; - const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate; - const factory DateCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate; + const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = + _DidReceiveCellUpdate; + const factory DateCellEvent.didReceiveFieldUpdate(FieldPB field) = + _DidReceiveFieldUpdate; } @freezed @@ -57,7 +62,7 @@ class DateCellState with _$DateCellState { const factory DateCellState({ required DateCellDataPB? data, required String dateStr, - required GridFieldPB field, + required FieldPB field, }) = _DateCellState; factory DateCellState.initial(GridDateCellController context) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_action_sheet_bloc.dart index 3caef12f73..52b3d3368e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_action_sheet_bloc.dart @@ -7,11 +7,13 @@ import 'field_service.dart'; part 'field_action_sheet_bloc.freezed.dart'; -class FieldActionSheetBloc extends Bloc { +class FieldActionSheetBloc + extends Bloc { final FieldService fieldService; - FieldActionSheetBloc({required GridFieldPB field, required this.fieldService}) - : super(FieldActionSheetState.initial(FieldTypeOptionDataPB.create()..field_2 = field)) { + FieldActionSheetBloc({required FieldPB field, required this.fieldService}) + : super(FieldActionSheetState.initial( + FieldTypeOptionDataPB.create()..field_2 = field)) { on( (event, emit) async { await event.map( @@ -57,7 +59,8 @@ class FieldActionSheetBloc extends Bloc FieldActionSheetState( + factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) => + FieldActionSheetState( fieldTypeOptionData: data, errorText: '', fieldName: data.field_2.name, diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart index 9597c871c1..e521f097ee 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_cache.dart @@ -8,18 +8,18 @@ import 'package:flutter/foundation.dart'; import '../row/row_cache.dart'; class FieldsNotifier extends ChangeNotifier { - List _fields = []; + List _fields = []; - set fields(List fields) { + set fields(List fields) { _fields = fields; notifyListeners(); } - List get fields => _fields; + List get fields => _fields; } -typedef FieldChangesetCallback = void Function(GridFieldChangesetPB); -typedef FieldsCallback = void Function(List); +typedef FieldChangesetCallback = void Function(FieldChangesetPB); +typedef FieldsCallback = void Function(List); class GridFieldCache { final String gridId; @@ -52,12 +52,12 @@ class GridFieldCache { _fieldNotifier = null; } - UnmodifiableListView get unmodifiableFields => + UnmodifiableListView get unmodifiableFields => UnmodifiableListView(_fieldNotifier?.fields ?? []); - List get fields => [..._fieldNotifier?.fields ?? []]; + List get fields => [..._fieldNotifier?.fields ?? []]; - set fields(List fields) { + set fields(List fields) { _fieldNotifier?.fields = [...fields]; } @@ -106,12 +106,12 @@ class GridFieldCache { } } - void _deleteFields(List deletedFields) { + void _deleteFields(List deletedFields) { if (deletedFields.isEmpty) { return; } - final List newFields = fields; - final Map deletedFieldMap = { + final List newFields = fields; + final Map deletedFieldMap = { for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder }; @@ -123,7 +123,7 @@ class GridFieldCache { if (insertedFields.isEmpty) { return; } - final List newFields = fields; + final List newFields = fields; for (final indexField in insertedFields) { if (newFields.length > indexField.index) { newFields.insert(indexField.index, indexField.field_1); @@ -134,11 +134,11 @@ class GridFieldCache { _fieldNotifier?.fields = newFields; } - void _updateFields(List updatedFields) { + void _updateFields(List updatedFields) { if (updatedFields.isEmpty) { return; } - final List newFields = fields; + final List newFields = fields; for (final updatedField in updatedFields) { final index = newFields.indexWhere((field) => field.id == updatedField.id); @@ -158,7 +158,7 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { GridRowFieldNotifierImpl(GridFieldCache cache) : _cache = cache; @override - UnmodifiableListView get fields => _cache.unmodifiableFields; + UnmodifiableListView get fields => _cache.unmodifiableFields; @override void onRowFieldsChanged(VoidCallback callback) { @@ -167,8 +167,8 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { } @override - void onRowFieldChanged(void Function(GridFieldPB) callback) { - _onChangesetFn = (GridFieldChangesetPB changeset) { + void onRowFieldChanged(void Function(FieldPB) callback) { + _onChangesetFn = (FieldChangesetPB changeset) { for (final updatedField in changeset.updatedFields) { callback(updatedField); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_cell_bloc.dart index ce09ba20bf..43114036c1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_cell_bloc.dart @@ -63,7 +63,7 @@ class FieldCellBloc extends Bloc { @freezed class FieldCellEvent with _$FieldCellEvent { const factory FieldCellEvent.initial() = _InitialCell; - const factory FieldCellEvent.didReceiveFieldUpdate(GridFieldPB field) = + const factory FieldCellEvent.didReceiveFieldUpdate(FieldPB field) = _DidReceiveFieldUpdate; const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth; @@ -74,7 +74,7 @@ class FieldCellEvent with _$FieldCellEvent { class FieldCellState with _$FieldCellState { const factory FieldCellState({ required String gridId, - required GridFieldPB field, + required FieldPB field, required double width, }) = _FieldCellState; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart index fc80964a87..aa40e98e92 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_editor_bloc.dart @@ -34,7 +34,7 @@ class FieldEditorBloc extends Bloc { dataController.fieldName = name; emit(state.copyWith(name: name)); }, - didReceiveFieldChanged: (GridFieldPB field) { + didReceiveFieldChanged: (FieldPB field) { emit(state.copyWith(field: Some(field))); }, ); @@ -52,7 +52,7 @@ class FieldEditorBloc extends Bloc { class FieldEditorEvent with _$FieldEditorEvent { const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.updateName(String name) = _UpdateName; - const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = + const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) = _DidReceiveFieldChanged; } @@ -62,7 +62,7 @@ class FieldEditorState with _$FieldEditorState { required String gridId, required String errorText, required String name, - required Option field, + required Option field, }) = _FieldEditorState; factory FieldEditorState.initial( diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart index d3b35e2bfc..b1bb885ae2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_listener.dart @@ -7,16 +7,18 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -typedef UpdateFieldNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = Either; class SingleFieldListener { final String fieldId; - PublishNotifier? _updateFieldNotifier = PublishNotifier(); + PublishNotifier? _updateFieldNotifier = + PublishNotifier(); GridNotificationListener? _listener; SingleFieldListener({required this.fieldId}); - void start({required void Function(UpdateFieldNotifiedValue) onFieldChanged}) { + void start( + {required void Function(UpdateFieldNotifiedValue) onFieldChanged}) { _updateFieldNotifier?.addPublishListener(onFieldChanged); _listener = GridNotificationListener( objectId: fieldId, @@ -31,7 +33,8 @@ class SingleFieldListener { switch (ty) { case GridNotification.DidUpdateField: result.fold( - (payload) => _updateFieldNotifier?.value = left(GridFieldPB.fromBuffer(payload)), + (payload) => + _updateFieldNotifier?.value = left(FieldPB.fromBuffer(payload)), (error) => _updateFieldNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index 39cdccb9ab..aca65dbc80 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -73,7 +73,7 @@ class FieldService { // Create the field if it does not exist. Otherwise, update the field. static Future> insertField({ required String gridId, - required GridFieldPB field, + required FieldPB field, List? typeOptionData, String? startFieldId, }) { @@ -121,7 +121,7 @@ class FieldService { Future> getFieldTypeOptionData({ required FieldType fieldType, }) { - final payload = GridFieldTypeOptionIdPB.create() + final payload = FieldTypeOptionIdPB.create() ..gridId = gridId ..fieldId = fieldId ..fieldType = fieldType; @@ -138,6 +138,6 @@ class FieldService { class GridFieldCellContext with _$GridFieldCellContext { const factory GridFieldCellContext({ required String gridId, - required GridFieldPB field, + required FieldPB field, }) = _GridFieldCellContext; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart index 5aa380cd72..254a371654 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_type_option_edit_bloc.dart @@ -42,14 +42,14 @@ class FieldTypeOptionEditBloc @freezed class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent { const factory FieldTypeOptionEditEvent.initial() = _Initial; - const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated( - GridFieldPB field) = _DidReceiveFieldUpdated; + const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(FieldPB field) = + _DidReceiveFieldUpdated; } @freezed class FieldTypeOptionEditState with _$FieldTypeOptionEditState { const factory FieldTypeOptionEditState({ - required GridFieldPB field, + required FieldPB field, }) = _FieldTypeOptionEditState; factory FieldTypeOptionEditState.initial( diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart index 67bec17be7..61d931e43e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/grid_listener.dart @@ -7,15 +7,17 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -typedef UpdateFieldNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = Either; class GridFieldsListener { final String gridId; - PublishNotifier? updateFieldsNotifier = PublishNotifier(); + PublishNotifier? updateFieldsNotifier = + PublishNotifier(); GridNotificationListener? _listener; GridFieldsListener({required this.gridId}); - void start({required void Function(UpdateFieldNotifiedValue) onFieldsChanged}) { + void start( + {required void Function(UpdateFieldNotifiedValue) onFieldsChanged}) { updateFieldsNotifier?.addPublishListener(onFieldsChanged); _listener = GridNotificationListener( objectId: gridId, @@ -27,7 +29,8 @@ class GridFieldsListener { switch (ty) { case GridNotification.DidUpdateGridField: result.fold( - (payload) => updateFieldsNotifier?.value = left(GridFieldChangesetPB.fromBuffer(payload)), + (payload) => updateFieldsNotifier?.value = + left(FieldChangesetPB.fromBuffer(payload)), (error) => updateFieldsNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart index e3cd38f396..9b04fcbed9 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart @@ -176,7 +176,7 @@ class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { class FieldTypeOptionLoader extends IFieldTypeOptionLoader { @override final String gridId; - final GridFieldPB field; + final FieldPB field; FieldTypeOptionLoader({ required this.gridId, @@ -185,7 +185,7 @@ class FieldTypeOptionLoader extends IFieldTypeOptionLoader { @override Future> load() { - final payload = GridFieldTypeOptionIdPB.create() + final payload = FieldTypeOptionIdPB.create() ..gridId = gridId ..fieldId = field.id ..fieldType = field.fieldType; diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart index b28cdcdb7d..66f4c35c20 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart @@ -12,12 +12,12 @@ class TypeOptionDataController { final String gridId; final IFieldTypeOptionLoader loader; late FieldTypeOptionDataPB _data; - final PublishNotifier _fieldNotifier = PublishNotifier(); + final PublishNotifier _fieldNotifier = PublishNotifier(); TypeOptionDataController({ required this.gridId, required this.loader, - GridFieldPB? field, + FieldPB? field, }) { if (field != null) { _data = FieldTypeOptionDataPB.create() @@ -42,11 +42,11 @@ class TypeOptionDataController { ); } - GridFieldPB get field { + FieldPB get field { return _data.field_2; } - set field(GridFieldPB field) { + set field(FieldPB field) { _updateData(newField: field); } @@ -64,7 +64,7 @@ class TypeOptionDataController { void _updateData({ String? newName, - GridFieldPB? newField, + FieldPB? newField, List? newTypeOptionData, }) { _data = _data.rebuild((rebuildData) { @@ -108,7 +108,7 @@ class TypeOptionDataController { }); } - void Function() addFieldListener(void Function(GridFieldPB) callback) { + void Function() addFieldListener(void Function(FieldPB) callback) { listener() { callback(field); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart index 44881b9f54..73d2079a6b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart @@ -97,11 +97,11 @@ class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; const factory GridEvent.didReceiveRowUpdate( - List rows, - GridRowChangeReason listState, + List rows, + RowChangeReason listState, ) = _DidReceiveRowUpdate; const factory GridEvent.didReceiveFieldUpdate( - UnmodifiableListView fields, + UnmodifiableListView fields, ) = _DidReceiveFieldUpdate; const factory GridEvent.didReceiveGridUpdate( @@ -115,9 +115,9 @@ class GridState with _$GridState { required String gridId, required Option grid, required GridFieldEquatable fields, - required List rowInfos, + required List rowInfos, required GridLoadingState loadingState, - required GridRowChangeReason reason, + required RowChangeReason reason, }) = _GridState; factory GridState.initial(String gridId) => GridState( @@ -138,9 +138,9 @@ class GridLoadingState with _$GridLoadingState { } class GridFieldEquatable extends Equatable { - final UnmodifiableListView _fields; + final UnmodifiableListView _fields; const GridFieldEquatable( - UnmodifiableListView fields, + UnmodifiableListView fields, ) : _fields = fields; @override @@ -157,5 +157,5 @@ class GridFieldEquatable extends Equatable { ]; } - UnmodifiableListView get value => UnmodifiableListView(_fields); + UnmodifiableListView get value => UnmodifiableListView(_fields); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 4833bc3ae0..dcbc67e530 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -13,12 +13,12 @@ import 'field/field_cache.dart'; import 'prelude.dart'; import 'row/row_cache.dart'; -typedef OnFieldsChanged = void Function(UnmodifiableListView); +typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef OnRowsChanged = void Function( - List rowInfos, - GridRowChangeReason, + List rowInfos, + RowChangeReason, ); typedef ListenONRowChangedCondition = bool Function(); @@ -36,8 +36,8 @@ class GridDataController { OnFieldsChanged? _onFieldsChanged; OnGridChanged? _onGridChanged; - List get rowInfos { - final List rows = []; + List get rowInfos { + final List rows = []; for (var block in _blocks.values) { rows.addAll(block.rows); } @@ -91,7 +91,7 @@ class GridDataController { } } - void _initialBlocks(List blocks) { + void _initialBlocks(List blocks) { for (final block in blocks) { if (_blocks[block.id] != null) { Log.warn("Initial duplicate block's cache: ${block.id}"); diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart index a0ab1aa343..125bd8d652 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart @@ -36,7 +36,7 @@ class GridHeaderBloc extends Bloc { Future _moveField( _MoveField value, Emitter emit) async { - final fields = List.from(state.fields); + final fields = List.from(state.fields); fields.insert(value.toIndex, fields.removeAt(value.fromIndex)); emit(state.copyWith(fields: fields)); @@ -64,19 +64,19 @@ class GridHeaderBloc extends Bloc { @freezed class GridHeaderEvent with _$GridHeaderEvent { const factory GridHeaderEvent.initial() = _InitialHeader; - const factory GridHeaderEvent.didReceiveFieldUpdate( - List fields) = _DidReceiveFieldUpdate; + const factory GridHeaderEvent.didReceiveFieldUpdate(List fields) = + _DidReceiveFieldUpdate; const factory GridHeaderEvent.moveField( - GridFieldPB field, int fromIndex, int toIndex) = _MoveField; + FieldPB field, int fromIndex, int toIndex) = _MoveField; } @freezed class GridHeaderState with _$GridHeaderState { - const factory GridHeaderState({required List fields}) = + const factory GridHeaderState({required List fields}) = _GridHeaderState; - factory GridHeaderState.initial(List fields) { - // final List newFields = List.from(fields); + factory GridHeaderState.initial(List fields) { + // final List newFields = List.from(fields); // newFields.retainWhere((field) => field.visibility); return GridHeaderState(fields: fields); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index 6eaaab7be8..5f95827d7e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -20,18 +20,17 @@ class GridService { return GridEventGetGrid(payload).send(); } - Future> createRow( - {Option? startRowId}) { + Future> createRow({Option? startRowId}) { CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId; startRowId?.fold(() => null, (id) => payload.startRowId = id); return GridEventCreateRow(payload).send(); } - Future> getFields( - {required List fieldIds}) { + Future> getFields( + {required List fieldIds}) { final payload = QueryFieldPayloadPB.create() ..gridId = gridId - ..fieldIds = RepeatedGridFieldIdPB(items: fieldIds); + ..fieldIds = RepeatedFieldIdPB(items: fieldIds); return GridEventGetFields(payload).send(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/prelude.dart b/frontend/app_flowy/lib/plugins/grid/application/prelude.dart index 2a19ca1134..7585c55e49 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/prelude.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/prelude.dart @@ -4,13 +4,13 @@ export 'row/row_service.dart'; export 'grid_service.dart'; export 'grid_header_bloc.dart'; -// GridFieldPB +// FieldPB export 'field/field_service.dart'; export 'field/field_action_sheet_bloc.dart'; export 'field/field_editor_bloc.dart'; export 'field/field_type_option_edit_bloc.dart'; -// GridFieldPB Type Option +// FieldPB Type Option export 'field/type_option/date_bloc.dart'; export 'field/type_option/number_bloc.dart'; export 'field/type_option/single_select_type_option.dart'; diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart index cedd426348..0b4499682f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart @@ -14,7 +14,7 @@ class RowActionSheetBloc extends Bloc { final RowService _rowService; - RowActionSheetBloc({required GridRowInfo rowData}) + RowActionSheetBloc({required RowInfo rowData}) : _rowService = RowService( gridId: rowData.gridId, blockId: rowData.blockId, @@ -56,11 +56,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent { @freezed class RowActionSheetState with _$RowActionSheetState { const factory RowActionSheetState({ - required GridRowInfo rowData, + required RowInfo rowData, }) = _RowActionSheetState; - factory RowActionSheetState.initial(GridRowInfo rowData) => - RowActionSheetState( + factory RowActionSheetState.initial(RowInfo rowData) => RowActionSheetState( rowData: rowData, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index b2579930ad..e6a68cd080 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -16,7 +16,7 @@ class RowBloc extends Bloc { final GridRowDataController _dataController; RowBloc({ - required GridRowInfo rowInfo, + required RowInfo rowInfo, required GridRowDataController dataController, }) : _rowService = RowService( gridId: rowInfo.gridId, @@ -72,19 +72,19 @@ class RowEvent with _$RowEvent { const factory RowEvent.initial() = _InitialRow; const factory RowEvent.createRow() = _CreateRow; const factory RowEvent.didReceiveCells( - GridCellMap gridCellMap, GridRowChangeReason reason) = _DidReceiveCells; + GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells; } @freezed class RowState with _$RowState { const factory RowState({ - required GridRowInfo rowInfo, + required RowInfo rowInfo, required GridCellMap gridCellMap, required UnmodifiableListView snapshots, - GridRowChangeReason? changeReason, + RowChangeReason? changeReason, }) = _RowState; - factory RowState.initial(GridRowInfo rowInfo, GridCellMap cellDataMap) => + factory RowState.initial(RowInfo rowInfo, GridCellMap cellDataMap) => RowState( rowInfo: rowInfo, gridCellMap: cellDataMap, @@ -94,9 +94,9 @@ class RowState with _$RowState { } class GridCellEquatable extends Equatable { - final GridFieldPB _field; + final FieldPB _field; - const GridCellEquatable(GridFieldPB field) : _field = field; + const GridCellEquatable(FieldPB field) : _field = field; @override List get props => [ diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index 4f63794afc..ec212148d1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -12,9 +12,9 @@ part 'row_cache.freezed.dart'; typedef RowUpdateCallback = void Function(); abstract class IGridRowFieldNotifier { - UnmodifiableListView get fields; + UnmodifiableListView get fields; void onRowFieldsChanged(VoidCallback callback); - void onRowFieldChanged(void Function(GridFieldPB) callback); + void onRowFieldChanged(void Function(FieldPB) callback); void onRowDispose(); } @@ -25,20 +25,20 @@ abstract class IGridRowFieldNotifier { class GridRowCache { final String gridId; - final GridBlockPB block; + final BlockPB block; /// _rows containers the current block's rows /// Use List to reverse the order of the GridRow. - List _rowInfos = []; + List _rowInfos = []; /// Use Map for faster access the raw row data. - final HashMap _rowByRowId; + final HashMap _rowByRowId; final GridCellCache _cellCache; final IGridRowFieldNotifier _fieldNotifier; - final _GridRowChangesetNotifier _rowChangeReasonNotifier; + final _RowChangesetNotifier _rowChangeReasonNotifier; - UnmodifiableListView get rows => UnmodifiableListView(_rowInfos); + UnmodifiableListView get rows => UnmodifiableListView(_rowInfos); GridCellCache get cellCache => _cellCache; GridRowCache({ @@ -47,11 +47,11 @@ class GridRowCache { required IGridRowFieldNotifier notifier, }) : _cellCache = GridCellCache(gridId: gridId), _rowByRowId = HashMap(), - _rowChangeReasonNotifier = _GridRowChangesetNotifier(), + _rowChangeReasonNotifier = _RowChangesetNotifier(), _fieldNotifier = notifier { // notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier - .receive(const GridRowChangeReason.fieldDidChange())); + .receive(const RowChangeReason.fieldDidChange())); notifier.onRowFieldChanged((field) => _cellCache.remove(field.id)); _rowInfos = block.rows .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble())) @@ -79,7 +79,7 @@ class GridRowCache { return; } - final List newRows = []; + final List newRows = []; final DeletedIndexs deletedIndex = []; final Map deletedRowByRowId = { for (var rowId in deletedRows) rowId: rowId @@ -94,7 +94,7 @@ class GridRowCache { } }); _rowInfos = newRows; - _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex)); + _rowChangeReasonNotifier.receive(RowChangeReason.delete(deletedIndex)); } void _insertRows(List insertRows) { @@ -113,7 +113,7 @@ class GridRowCache { (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); } - _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs)); + _rowChangeReasonNotifier.receive(RowChangeReason.insert(insertIndexs)); } void _updateRows(List updatedRows) { @@ -135,7 +135,7 @@ class GridRowCache { } } - _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs)); + _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs)); } void _hideRows(List hideRows) {} @@ -143,7 +143,7 @@ class GridRowCache { void _showRows(List visibleRows) {} void onRowsChanged( - void Function(GridRowChangeReason) onRowChanged, + void Function(RowChangeReason) onRowChanged, ) { _rowChangeReasonNotifier.addListener(() { onRowChanged(_rowChangeReasonNotifier.reason); @@ -152,7 +152,7 @@ class GridRowCache { RowUpdateCallback addListener({ required String rowId, - void Function(GridCellMap, GridRowChangeReason)? onCellUpdated, + void Function(GridCellMap, RowChangeReason)? onCellUpdated, bool Function()? listenWhen, }) { listenerHandler() async { @@ -187,7 +187,7 @@ class GridRowCache { } GridCellMap loadGridCells(String rowId) { - final GridRowPB? data = _rowByRowId[rowId]; + final RowPB? data = _rowByRowId[rowId]; if (data == null) { _loadRow(rowId); } @@ -195,7 +195,7 @@ class GridRowCache { } Future _loadRow(String rowId) async { - final payload = GridRowIdPB.create() + final payload = RowIdPB.create() ..gridId = gridId ..blockId = block.id ..rowId = rowId; @@ -207,7 +207,7 @@ class GridRowCache { ); } - GridCellMap _makeGridCells(String rowId, GridRowPB? row) { + GridCellMap _makeGridCells(String rowId, RowPB? row) { var cellDataMap = GridCellMap.new(); for (final field in _fieldNotifier.fields) { if (field.visibility) { @@ -242,14 +242,13 @@ class GridRowCache { updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id); // - _rowChangeReasonNotifier - .receive(GridRowChangeReason.update(updatedIndexs)); + _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs)); } } } - GridRowInfo buildGridRow(String rowId, double rowHeight) { - return GridRowInfo( + RowInfo buildGridRow(String rowId, double rowHeight) { + return RowInfo( gridId: gridId, blockId: block.id, fields: _fieldNotifier.fields, @@ -259,12 +258,12 @@ class GridRowCache { } } -class _GridRowChangesetNotifier extends ChangeNotifier { - GridRowChangeReason reason = const InitialListState(); +class _RowChangesetNotifier extends ChangeNotifier { + RowChangeReason reason = const InitialListState(); - _GridRowChangesetNotifier(); + _RowChangesetNotifier(); - void receive(GridRowChangeReason newReason) { + void receive(RowChangeReason newReason) { reason = newReason; reason.map( insert: (_) => notifyListeners(), @@ -277,15 +276,15 @@ class _GridRowChangesetNotifier extends ChangeNotifier { } @freezed -class GridRowInfo with _$GridRowInfo { - const factory GridRowInfo({ +class RowInfo with _$RowInfo { + const factory RowInfo({ required String gridId, required String blockId, required String id, - required UnmodifiableListView fields, + required UnmodifiableListView fields, required double height, - GridRowPB? rawRow, - }) = _GridRowInfo; + RowPB? rawRow, + }) = _RowInfo; } typedef InsertedIndexs = List; @@ -293,12 +292,12 @@ typedef DeletedIndexs = List; typedef UpdatedIndexs = LinkedHashMap; @freezed -class GridRowChangeReason with _$GridRowChangeReason { - const factory GridRowChangeReason.insert(InsertedIndexs items) = _Insert; - const factory GridRowChangeReason.delete(DeletedIndexs items) = _Delete; - const factory GridRowChangeReason.update(UpdatedIndexs indexs) = _Update; - const factory GridRowChangeReason.fieldDidChange() = _FieldDidChange; - const factory GridRowChangeReason.initial() = InitialListState; +class RowChangeReason with _$RowChangeReason { + const factory RowChangeReason.insert(InsertedIndexs items) = _Insert; + const factory RowChangeReason.delete(DeletedIndexs items) = _Delete; + const factory RowChangeReason.update(UpdatedIndexs indexs) = _Update; + const factory RowChangeReason.fieldDidChange() = _FieldDidChange; + const factory RowChangeReason.initial() = InitialListState; } class InsertedIndex { @@ -312,7 +311,7 @@ class InsertedIndex { class DeletedIndex { final int index; - final GridRowInfo row; + final RowInfo row; DeletedIndex({ required this.index, required this.row, diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart index 3f25e414f1..31a54aa29b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -5,10 +5,10 @@ import '../cell/cell_service/cell_service.dart'; import '../field/field_cache.dart'; import 'row_cache.dart'; -typedef OnRowChanged = void Function(GridCellMap, GridRowChangeReason); +typedef OnRowChanged = void Function(GridCellMap, RowChangeReason); class GridRowDataController extends GridCellBuilderDelegate { - final GridRowInfo rowInfo; + final RowInfo rowInfo; final List _onRowChangedListeners = []; final GridFieldCache _fieldCache; final GridRowCache _rowCache; diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart index 9aa829d617..1df24c50c2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_listener.dart @@ -8,12 +8,13 @@ import 'dart:typed_data'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -typedef UpdateRowNotifiedValue = Either; -typedef UpdateFieldNotifiedValue = Either, FlowyError>; +typedef UpdateRowNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = Either, FlowyError>; class RowListener { final String rowId; - PublishNotifier? updateRowNotifier = PublishNotifier(); + PublishNotifier? updateRowNotifier = + PublishNotifier(); GridNotificationListener? _listener; RowListener({required this.rowId}); @@ -26,7 +27,8 @@ class RowListener { switch (ty) { case GridNotification.DidUpdateRow: result.fold( - (payload) => updateRowNotifier?.value = left(GridRowPB.fromBuffer(payload)), + (payload) => + updateRowNotifier?.value = left(RowPB.fromBuffer(payload)), (error) => updateRowNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index 2dce917da4..94e047c1f7 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -13,7 +13,7 @@ class RowService { RowService( {required this.gridId, required this.blockId, required this.rowId}); - Future> createRow() { + Future> createRow() { CreateRowPayloadPB payload = CreateRowPayloadPB.create() ..gridId = gridId ..startRowId = rowId; @@ -34,7 +34,7 @@ class RowService { } Future> getRow() { - final payload = GridRowIdPB.create() + final payload = RowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -43,7 +43,7 @@ class RowService { } Future> deleteRow() { - final payload = GridRowIdPB.create() + final payload = RowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -52,7 +52,7 @@ class RowService { } Future> duplicateRow() { - final payload = GridRowIdPB.create() + final payload = RowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; diff --git a/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart index 972b64f69a..fc0c2b4b12 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart @@ -11,7 +11,7 @@ part 'property_bloc.freezed.dart'; class GridPropertyBloc extends Bloc { final GridFieldCache _fieldCache; - Function(List)? _onFieldsFn; + Function(List)? _onFieldsFn; GridPropertyBloc({required String gridId, required GridFieldCache fieldCache}) : _fieldCache = fieldCache, @@ -67,8 +67,8 @@ class GridPropertyEvent with _$GridPropertyEvent { const factory GridPropertyEvent.initial() = _Initial; const factory GridPropertyEvent.setFieldVisibility( String fieldId, bool visibility) = _SetFieldVisibility; - const factory GridPropertyEvent.didReceiveFieldUpdate( - List fields) = _DidReceiveFieldUpdate; + const factory GridPropertyEvent.didReceiveFieldUpdate(List fields) = + _DidReceiveFieldUpdate; const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) = _MoveField; } @@ -77,10 +77,10 @@ class GridPropertyEvent with _$GridPropertyEvent { class GridPropertyState with _$GridPropertyState { const factory GridPropertyState({ required String gridId, - required List fields, + required List fields, }) = _GridPropertyState; - factory GridPropertyState.initial(String gridId, List fields) => + factory GridPropertyState.initial(String gridId, List fields) => GridPropertyState( gridId: gridId, fields: fields, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index 3d3be83c79..8709b395b2 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -225,7 +225,7 @@ class _GridRowsState extends State<_GridRows> { initialItemCount: context.read().state.rowInfos.length, itemBuilder: (BuildContext context, int index, Animation animation) { - final GridRowInfo rowInfo = + final RowInfo rowInfo = context.read().state.rowInfos[index]; return _renderRow(context, rowInfo, animation); }, @@ -236,7 +236,7 @@ class _GridRowsState extends State<_GridRows> { Widget _renderRow( BuildContext context, - GridRowInfo rowInfo, + RowInfo rowInfo, Animation animation, ) { final rowCache = @@ -274,7 +274,7 @@ class _GridRowsState extends State<_GridRows> { void _openRowDetailPage( BuildContext context, - GridRowInfo rowInfo, + RowInfo rowInfo, GridFieldCache fieldCache, GridRowCache rowCache, GridCellBuilder cellBuilder, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart b/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart index 0b289ecd4b..e47b47a267 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart @@ -2,11 +2,15 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'sizes.dart'; class GridLayout { - static double headerWidth(List fields) { + static double headerWidth(List fields) { if (fields.isEmpty) return 0; - final fieldsWidth = fields.map((field) => field.width.toDouble()).reduce((value, element) => value + element); + final fieldsWidth = fields + .map((field) => field.width.toDouble()) + .reduce((value, element) => value + element); - return fieldsWidth + GridSize.leadingHeaderPadding + GridSize.trailHeaderPadding; + return fieldsWidth + + GridSize.leadingHeaderPadding + + GridSize.trailHeaderPadding; } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart index cb8df60591..c1de0eca31 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart @@ -144,7 +144,7 @@ class _DragToExpandLine extends StatelessWidget { class FieldCellButton extends StatelessWidget { final VoidCallback onTap; - final GridFieldPB field; + final FieldPB field; const FieldCellButton({ required this.field, required this.onTap, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart index 50a218fd0d..20440235cb 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart @@ -16,7 +16,7 @@ import 'field_type_extension.dart'; import 'field_type_list.dart'; import 'type_option/builder.dart'; -typedef UpdateFieldCallback = void Function(GridFieldPB, Uint8List); +typedef UpdateFieldCallback = void Function(FieldPB, Uint8List); typedef SwitchToFieldCallback = Future> Function( String fieldId, @@ -64,7 +64,7 @@ class _FieldTypeOptionEditorState extends State { ); } - Widget _switchFieldTypeButton(BuildContext context, GridFieldPB field) { + Widget _switchFieldTypeButton(BuildContext context, FieldPB field) { final theme = context.watch(); return SizedBox( height: GridSize.typeOptionItemHeight, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart index 4b5f364c0d..1a8ed0be20 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart @@ -170,7 +170,7 @@ class CreateFieldButton extends StatelessWidget { class SliverHeaderDelegateImplementation extends SliverPersistentHeaderDelegate { final String gridId; - final List fields; + final List fields; SliverHeaderDelegateImplementation( {required this.gridId, required this.fields}); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 40ce6fb82e..464c2f847c 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -131,7 +131,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ TypeOptionContext makeTypeOptionContext({ required String gridId, - required GridFieldPB field, + required FieldPB field, }) { final loader = FieldTypeOptionLoader(gridId: gridId, field: field); final dataController = TypeOptionDataController( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 044ddbb5ee..6c995d57eb 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -16,7 +16,7 @@ import '../cell/prelude.dart'; import 'row_action_sheet.dart'; class GridRowWidget extends StatefulWidget { - final GridRowInfo rowInfo; + final RowInfo rowInfo; final GridRowDataController dataController; final GridCellBuilder cellBuilder; final void Function(BuildContext, GridCellBuilder) openDetailPage; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart index 9c31bab5f3..720aac0dc0 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart @@ -15,7 +15,7 @@ import '../../../application/row/row_cache.dart'; import '../../layout/sizes.dart'; class GridRowActionSheet extends StatelessWidget { - final GridRowInfo rowData; + final RowInfo rowData; const GridRowActionSheet({required this.rowData, Key? key}) : super(key: key); @override diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index b49cce1f11..02230e5139 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart @@ -78,7 +78,7 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate { } class _GridPropertyCell extends StatelessWidget { - final GridFieldPB field; + final FieldPB field; final String gridId; const _GridPropertyCell({required this.gridId, required this.field, Key? key}) : super(key: key); diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart index b5d364f67e..d012b0e7ca 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart @@ -1632,7 +1632,7 @@ final Map activities = Map.fromIterables([ 'Flying Disc', 'Bowling', 'Cricket Game', - 'GridFieldPB Hockey', + 'FieldPB Hockey', 'Ice Hockey', 'Lacrosse', 'Ping Pong', diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index f778186903..ad89532b02 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -4,24 +4,24 @@ use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::RowRevision; use std::sync::Arc; -/// [GridBlockPB] contains list of row ids. The rows here does not contain any data, just the id -/// of the row. Check out [GridRowPB] for more details. +/// [BlockPB] contains list of row ids. The rows here does not contain any data, just the id +/// of the row. Check out [RowPB] for more details. /// /// /// A grid can have many rows. Rows are therefore grouped into Blocks in order to make /// things more efficient. /// | #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridBlockPB { +pub struct BlockPB { #[pb(index = 1)] pub id: String, #[pb(index = 2)] - pub rows: Vec, + pub rows: Vec, } -impl GridBlockPB { - pub fn new(block_id: &str, rows: Vec) -> Self { +impl BlockPB { + pub fn new(block_id: &str, rows: Vec) -> Self { Self { id: block_id.to_owned(), rows, @@ -29,9 +29,9 @@ impl GridBlockPB { } } -/// [GridRowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. +/// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. #[derive(Debug, Default, Clone, ProtoBuf)] -pub struct GridRowPB { +pub struct RowPB { #[pb(index = 1)] pub block_id: String, @@ -42,7 +42,7 @@ pub struct GridRowPB { pub height: i32, } -impl GridRowPB { +impl RowPB { pub fn row_id(&self) -> &str { &self.id } @@ -52,7 +52,7 @@ impl GridRowPB { } } -impl std::convert::From<&RowRevision> for GridRowPB { +impl std::convert::From<&RowRevision> for RowPB { fn from(rev: &RowRevision) -> Self { Self { block_id: rev.block_id.clone(), @@ -62,7 +62,7 @@ impl std::convert::From<&RowRevision> for GridRowPB { } } -impl std::convert::From<&Arc> for GridRowPB { +impl std::convert::From<&Arc> for RowPB { fn from(rev: &Arc) -> Self { Self { block_id: rev.block_id.clone(), @@ -75,30 +75,30 @@ impl std::convert::From<&Arc> for GridRowPB { #[derive(Debug, Default, ProtoBuf)] pub struct OptionalRowPB { #[pb(index = 1, one_of)] - pub row: Option, + pub row: Option, } #[derive(Debug, Default, ProtoBuf)] pub struct RepeatedRowPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From> for RepeatedRowPB { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedRowPB { + fn from(items: Vec) -> Self { Self { items } } } -/// [RepeatedGridBlockPB] contains list of [GridBlockPB] +/// [RepeatedBlockPB] contains list of [BlockPB] #[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedGridBlockPB { +pub struct RepeatedBlockPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From> for RepeatedGridBlockPB { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedBlockPB { + fn from(items: Vec) -> Self { Self { items } } } @@ -127,11 +127,11 @@ pub struct UpdatedRowPB { pub row_id: String, #[pb(index = 3)] - pub row: GridRowPB, + pub row: RowPB, } impl UpdatedRowPB { - pub fn new(row_rev: &RowRevision, row: GridRowPB) -> Self { + pub fn new(row_rev: &RowRevision, row: RowPB) -> Self { Self { row_id: row_rev.id.clone(), block_id: row_rev.block_id.clone(), @@ -140,8 +140,8 @@ impl UpdatedRowPB { } } -impl std::convert::From for InsertedRowPB { - fn from(row_info: GridRowPB) -> Self { +impl std::convert::From for InsertedRowPB { + fn from(row_info: RowPB) -> Self { Self { row_id: row_info.id, block_id: row_info.block_id, @@ -153,7 +153,7 @@ impl std::convert::From for InsertedRowPB { impl std::convert::From<&RowRevision> for InsertedRowPB { fn from(row: &RowRevision) -> Self { - let row_order = GridRowPB::from(row); + let row_order = RowPB::from(row); Self::from(row_order) } } @@ -204,10 +204,10 @@ impl GridBlockChangesetPB { } } -/// [QueryGridBlocksPayloadPB] is used to query the data of the block that belongs to the grid whose +/// [QueryBlocksPayloadPB] is used to query the data of the block that belongs to the grid whose /// id is grid_id. #[derive(ProtoBuf, Default)] -pub struct QueryGridBlocksPayloadPB { +pub struct QueryBlocksPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -220,7 +220,7 @@ pub struct QueryGridBlocksParams { pub block_ids: Vec, } -impl TryInto for QueryGridBlocksPayloadPB { +impl TryInto for QueryBlocksPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index dd31e22a54..468b216b5b 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -8,9 +8,9 @@ use std::sync::Arc; use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter, EnumString}; -/// [GridFieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc. +/// [FieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc. #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridFieldPB { +pub struct FieldPB { #[pb(index = 1)] pub id: String, @@ -36,7 +36,7 @@ pub struct GridFieldPB { pub is_primary: bool, } -impl std::convert::From for GridFieldPB { +impl std::convert::From for FieldPB { fn from(field_rev: FieldRevision) -> Self { Self { id: field_rev.id, @@ -51,33 +51,33 @@ impl std::convert::From for GridFieldPB { } } -impl std::convert::From> for GridFieldPB { +impl std::convert::From> for FieldPB { fn from(field_rev: Arc) -> Self { let field_rev = field_rev.as_ref().clone(); - GridFieldPB::from(field_rev) + FieldPB::from(field_rev) } } -/// [GridFieldIdPB] id of the [Field] +/// [FieldIdPB] id of the [Field] #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridFieldIdPB { +pub struct FieldIdPB { #[pb(index = 1)] pub field_id: String, } -impl std::convert::From<&str> for GridFieldIdPB { +impl std::convert::From<&str> for FieldIdPB { fn from(s: &str) -> Self { - GridFieldIdPB { field_id: s.to_owned() } + FieldIdPB { field_id: s.to_owned() } } } -impl std::convert::From for GridFieldIdPB { +impl std::convert::From for FieldIdPB { fn from(s: String) -> Self { - GridFieldIdPB { field_id: s } + FieldIdPB { field_id: s } } } -impl std::convert::From<&Arc> for GridFieldIdPB { +impl std::convert::From<&Arc> for FieldIdPB { fn from(field_rev: &Arc) -> Self { Self { field_id: field_rev.id.clone(), @@ -85,7 +85,7 @@ impl std::convert::From<&Arc> for GridFieldIdPB { } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridFieldChangesetPB { +pub struct FieldChangesetPB { #[pb(index = 1)] pub grid_id: String, @@ -93,13 +93,13 @@ pub struct GridFieldChangesetPB { pub inserted_fields: Vec, #[pb(index = 3)] - pub deleted_fields: Vec, + pub deleted_fields: Vec, #[pb(index = 4)] - pub updated_fields: Vec, + pub updated_fields: Vec, } -impl GridFieldChangesetPB { +impl FieldChangesetPB { pub fn insert(grid_id: &str, inserted_fields: Vec) -> Self { Self { grid_id: grid_id.to_owned(), @@ -109,7 +109,7 @@ impl GridFieldChangesetPB { } } - pub fn delete(grid_id: &str, deleted_fields: Vec) -> Self { + pub fn delete(grid_id: &str, deleted_fields: Vec) -> Self { Self { grid_id: grid_id.to_string(), inserted_fields: vec![], @@ -118,7 +118,7 @@ impl GridFieldChangesetPB { } } - pub fn update(grid_id: &str, updated_fields: Vec) -> Self { + pub fn update(grid_id: &str, updated_fields: Vec) -> Self { Self { grid_id: grid_id.to_string(), inserted_fields: vec![], @@ -131,7 +131,7 @@ impl GridFieldChangesetPB { #[derive(Debug, Clone, Default, ProtoBuf)] pub struct IndexFieldPB { #[pb(index = 1)] - pub field: GridFieldPB, + pub field: FieldPB, #[pb(index = 2)] pub index: i32, @@ -140,7 +140,7 @@ pub struct IndexFieldPB { impl IndexFieldPB { pub fn from_field_rev(field_rev: &Arc, index: usize) -> Self { Self { - field: GridFieldPB::from(field_rev.as_ref().clone()), + field: FieldPB::from(field_rev.as_ref().clone()), index: index as i32, } } @@ -220,7 +220,7 @@ impl TryInto for EditFieldPayloadPB { } #[derive(Debug, Default, ProtoBuf)] -pub struct GridFieldTypeOptionIdPB { +pub struct FieldTypeOptionIdPB { #[pb(index = 1)] pub grid_id: String, @@ -231,19 +231,19 @@ pub struct GridFieldTypeOptionIdPB { pub field_type: FieldType, } -pub struct GridFieldTypeOptionIdParams { +pub struct FieldTypeOptionIdParams { pub grid_id: String, pub field_id: String, pub field_type: FieldType, } -impl TryInto for GridFieldTypeOptionIdPB { +impl TryInto for FieldTypeOptionIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; - Ok(GridFieldTypeOptionIdParams { + Ok(FieldTypeOptionIdParams { grid_id: grid_id.0, field_id: field_id.0, field_type: self.field_type, @@ -264,60 +264,60 @@ pub struct FieldTypeOptionDataPB { pub grid_id: String, #[pb(index = 2)] - pub field: GridFieldPB, + pub field: FieldPB, #[pb(index = 3)] pub type_option_data: Vec, } -/// Collection of the [GridFieldPB] +/// Collection of the [FieldPB] #[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedGridFieldPB { +pub struct RepeatedFieldPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::ops::Deref for RepeatedGridFieldPB { - type Target = Vec; +impl std::ops::Deref for RepeatedFieldPB { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl std::ops::DerefMut for RepeatedGridFieldPB { +impl std::ops::DerefMut for RepeatedFieldPB { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } } -impl std::convert::From> for RepeatedGridFieldPB { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedFieldPB { + fn from(items: Vec) -> Self { Self { items } } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RepeatedGridFieldIdPB { +pub struct RepeatedFieldIdPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::ops::Deref for RepeatedGridFieldIdPB { - type Target = Vec; +impl std::ops::Deref for RepeatedFieldIdPB { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl std::convert::From> for RepeatedGridFieldIdPB { - fn from(items: Vec) -> Self { - RepeatedGridFieldIdPB { items } +impl std::convert::From> for RepeatedFieldIdPB { + fn from(items: Vec) -> Self { + RepeatedFieldIdPB { items } } } -impl std::convert::From for RepeatedGridFieldIdPB { +impl std::convert::From for RepeatedFieldIdPB { fn from(s: String) -> Self { - RepeatedGridFieldIdPB { - items: vec![GridFieldIdPB::from(s)], + RepeatedFieldIdPB { + items: vec![FieldIdPB::from(s)], } } } @@ -328,7 +328,7 @@ pub struct InsertFieldPayloadPB { pub grid_id: String, #[pb(index = 2)] - pub field: GridFieldPB, + pub field: FieldPB, #[pb(index = 3)] pub type_option_data: Vec, @@ -340,7 +340,7 @@ pub struct InsertFieldPayloadPB { #[derive(Clone)] pub struct InsertFieldParams { pub grid_id: String, - pub field: GridFieldPB, + pub field: FieldPB, pub type_option_data: Vec, pub start_field_id: Option, } @@ -408,12 +408,12 @@ pub struct QueryFieldPayloadPB { pub grid_id: String, #[pb(index = 2)] - pub field_ids: RepeatedGridFieldIdPB, + pub field_ids: RepeatedFieldIdPB, } pub struct QueryFieldParams { pub grid_id: String, - pub field_ids: RepeatedGridFieldIdPB, + pub field_ids: RepeatedFieldIdPB, } impl TryInto for QueryFieldPayloadPB { @@ -633,13 +633,13 @@ pub struct GridFieldIdentifierPayloadPB { pub grid_id: String, } -impl TryInto for DuplicateFieldPayloadPB { +impl TryInto for DuplicateFieldPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; - Ok(GridFieldIdParams { + Ok(FieldIdParams { grid_id: grid_id.0, field_id: field_id.0, }) @@ -655,20 +655,20 @@ pub struct DeleteFieldPayloadPB { pub grid_id: String, } -impl TryInto for DeleteFieldPayloadPB { +impl TryInto for DeleteFieldPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; - Ok(GridFieldIdParams { + Ok(FieldIdParams { grid_id: grid_id.0, field_id: field_id.0, }) } } -pub struct GridFieldIdParams { +pub struct FieldIdParams { pub field_id: String, pub grid_id: String, } diff --git a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs index 49278afc54..6657e0e05a 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs @@ -1,4 +1,4 @@ -use crate::entities::{GridBlockPB, GridFieldIdPB}; +use crate::entities::{BlockPB, FieldIdPB}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -10,10 +10,10 @@ pub struct GridPB { pub id: String, #[pb(index = 2)] - pub fields: Vec, + pub fields: Vec, #[pb(index = 3)] - pub blocks: Vec, + pub blocks: Vec, } #[derive(ProtoBuf, Default)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index cc89a11e59..11f338de63 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, GridRowPB}; +use crate::entities::{FieldType, RowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -44,7 +44,7 @@ pub struct GroupPB { pub desc: String, #[pb(index = 3)] - pub rows: Vec, + pub rows: Vec, } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs index 745a5dc368..47af2e00d3 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs @@ -3,7 +3,7 @@ use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; #[derive(Debug, Default, Clone, ProtoBuf)] -pub struct GridRowIdPB { +pub struct RowIdPB { #[pb(index = 1)] pub grid_id: String, @@ -14,21 +14,21 @@ pub struct GridRowIdPB { pub row_id: String, } -pub struct GridRowIdParams { +pub struct RowIdParams { pub grid_id: String, pub block_id: String, pub row_id: String, } -impl TryInto for GridRowIdPB { +impl TryInto for RowIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let block_id = NotEmptyStr::parse(self.block_id).map_err(|_| ErrorCode::BlockIdIsEmpty)?; let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - Ok(GridRowIdParams { + Ok(RowIdParams { grid_id: grid_id.0, block_id: block_id.0, row_id: row_id.0, diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 496ee5ed18..4721e70bca 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -49,9 +49,9 @@ pub(crate) async fn update_grid_setting_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_grid_blocks_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: QueryGridBlocksParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let repeated_grid_block = editor.get_blocks(Some(params.block_ids)).await?; @@ -62,7 +62,7 @@ pub(crate) async fn get_grid_blocks_handler( pub(crate) async fn get_fields_handler( data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: QueryFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let field_orders = params @@ -72,7 +72,7 @@ pub(crate) async fn get_fields_handler( .map(|field_order| field_order.field_id) .collect(); let field_revs = editor.get_field_revs(Some(field_orders)).await?; - let repeated_field: RepeatedGridFieldPB = field_revs.into_iter().map(GridFieldPB::from).collect::>().into(); + let repeated_field: RepeatedFieldPB = field_revs.into_iter().map(FieldPB::from).collect::>().into(); data_result(repeated_field) } @@ -116,7 +116,7 @@ pub(crate) async fn delete_field_handler( data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridFieldIdParams = data.into_inner().try_into()?; + let params: FieldIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.delete_field(¶ms.field_id).await?; Ok(()) @@ -154,7 +154,7 @@ pub(crate) async fn duplicate_field_handler( data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridFieldIdParams = data.into_inner().try_into()?; + let params: FieldIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.duplicate_field(¶ms.field_id).await?; Ok(()) @@ -163,10 +163,10 @@ pub(crate) async fn duplicate_field_handler( /// 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: Data, + data: Data, manager: AppData>, ) -> DataResult { - let params: GridFieldTypeOptionIdParams = data.into_inner().try_into()?; + let params: FieldTypeOptionIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_rev(¶ms.field_id).await { None => Err(FlowyError::record_not_found()), @@ -227,10 +227,10 @@ async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> DataResult { - let params: GridRowIdParams = data.into_inner().try_into()?; + let params: RowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let row = editor .get_row_rev(¶ms.row_id) @@ -242,10 +242,10 @@ pub(crate) async fn get_row_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn delete_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridRowIdParams = data.into_inner().try_into()?; + let params: RowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.delete_row(¶ms.row_id).await?; Ok(()) @@ -253,10 +253,10 @@ pub(crate) async fn delete_row_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn duplicate_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridRowIdParams = data.into_inner().try_into()?; + let params: RowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.duplicate_row(¶ms.row_id).await?; Ok(()) diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index f9b0770174..0852deade2 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -57,9 +57,9 @@ pub enum GridEvent { /// [GetGridBlocks] event is used to get the grid's block. /// - /// The event handler accepts a [QueryGridBlocksPayloadPB] and returns a [RepeatedGridBlockPB] + /// The event handler accepts a [QueryBlocksPayloadPB] and returns a [RepeatedBlockPB] /// if there are no errors. - #[event(input = "QueryGridBlocksPayloadPB", output = "RepeatedGridBlockPB")] + #[event(input = "QueryBlocksPayloadPB", output = "RepeatedBlockPB")] GetGridBlocks = 1, /// [GetGridSetting] event is used to get the grid's settings. @@ -77,9 +77,9 @@ pub enum GridEvent { /// [GetFields] event is used to get the grid's settings. /// - /// The event handler accepts a [QueryFieldPayloadPB] and returns a [RepeatedGridFieldPB] + /// The event handler accepts a [QueryFieldPayloadPB] and returns a [RepeatedFieldPB] /// if there are no errors. - #[event(input = "QueryFieldPayloadPB", output = "RepeatedGridFieldPB")] + #[event(input = "QueryFieldPayloadPB", output = "RepeatedFieldPB")] GetFields = 10, /// [UpdateField] event is used to update a field's attributes. @@ -132,13 +132,13 @@ pub enum GridEvent { #[event(input = "MoveItemPayloadPB")] MoveItem = 22, - /// [GetFieldTypeOption] event is used to get the FieldTypeOption data for a specific field type. + /// [FieldTypeOptionIdPB] event is used to get the FieldTypeOption data for a specific field type. /// /// Check out the [FieldTypeOptionDataPB] for more details. If the [FieldTypeOptionData] does exist /// for the target type, the [TypeOptionBuilder] will create the default data for that type. /// /// Return the [FieldTypeOptionDataPB] if there are no errors. - #[event(input = "GridFieldTypeOptionIdPB", output = "FieldTypeOptionDataPB")] + #[event(input = "FieldTypeOptionIdPB", output = "FieldTypeOptionDataPB")] GetFieldTypeOption = 23, /// [CreateFieldTypeOption] event is used to create a new FieldTypeOptionData. @@ -165,18 +165,18 @@ pub enum GridEvent { #[event(input = "SelectOptionChangesetPayloadPB")] UpdateSelectOption = 32, - #[event(input = "CreateRowPayloadPB", output = "GridRowPB")] + #[event(input = "CreateRowPayloadPB", output = "RowPB")] CreateRow = 50, - /// [GetRow] event is used to get the row data,[GridRowPB]. [OptionalRowPB] is a wrapper that enables + /// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables /// to return a nullable row data. - #[event(input = "GridRowIdPB", output = "OptionalRowPB")] + #[event(input = "RowIdPB", output = "OptionalRowPB")] GetRow = 51, - #[event(input = "GridRowIdPB")] + #[event(input = "RowIdPB")] DeleteRow = 52, - #[event(input = "GridRowIdPB")] + #[event(input = "RowIdPB")] DuplicateRow = 53, #[event(input = "GridCellIdPB", output = "GridCellPB")] diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index c83bfcc2cd..d7c01a6a09 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -1,5 +1,5 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::{CellChangesetPB, GridBlockChangesetPB, GridRowPB, InsertedRowPB, UpdatedRowPB}; +use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB, UpdatedRowPB}; use crate::manager::GridUser; use crate::services::block_revision_editor::{GridBlockRevisionCompactor, GridBlockRevisionEditor}; use crate::services::persistence::block_index::BlockIndexCache; @@ -110,7 +110,7 @@ impl GridBlockManager { pub async fn update_row(&self, changeset: RowMetaChangeset, row_builder: F) -> FlowyResult<()> where - F: FnOnce(Arc) -> Option, + F: FnOnce(Arc) -> Option, { let editor = self.get_editor_from_row_id(&changeset.row_id).await?; let _ = editor.update_row(changeset.clone()).await?; @@ -146,10 +146,7 @@ impl GridBlockManager { Ok(()) } - pub(crate) async fn delete_rows( - &self, - row_orders: Vec, - ) -> FlowyResult> { + pub(crate) async fn delete_rows(&self, row_orders: Vec) -> FlowyResult> { let mut changesets = vec![]; for grid_block in block_from_row_orders(row_orders) { let editor = self.get_editor(&grid_block.id).await?; @@ -198,7 +195,7 @@ impl GridBlockManager { pub async fn update_cell(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()> where - F: FnOnce(Arc) -> Option, + F: FnOnce(Arc) -> Option, { let row_changeset: RowMetaChangeset = changeset.clone().into(); let _ = self.update_row(row_changeset, row_builder).await?; @@ -217,7 +214,7 @@ impl GridBlockManager { } } - pub async fn get_row_orders(&self, block_id: &str) -> FlowyResult> { + pub async fn get_row_orders(&self, block_id: &str) -> FlowyResult> { let editor = self.get_editor(block_id).await?; editor.get_row_infos::<&str>(None).await } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs index a9f68c5776..0d5f5d206e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs @@ -1,4 +1,4 @@ -use crate::entities::GridRowPB; +use crate::entities::RowPB; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision}; @@ -123,12 +123,12 @@ impl GridBlockRevisionEditor { Ok(cell_revs) } - pub async fn get_row_info(&self, row_id: &str) -> FlowyResult> { + pub async fn get_row_info(&self, row_id: &str) -> FlowyResult> { let row_ids = Some(vec![Cow::Borrowed(row_id)]); Ok(self.get_row_infos(row_ids).await?.pop()) } - pub async fn get_row_infos(&self, row_ids: Option>>) -> FlowyResult> + pub async fn get_row_infos(&self, row_ids: Option>>) -> FlowyResult> where T: AsRef + ToOwned + ?Sized, { @@ -138,8 +138,8 @@ impl GridBlockRevisionEditor { .await .get_row_revs(row_ids)? .iter() - .map(GridRowPB::from) - .collect::>(); + .map(RowPB::from) + .collect::>(); Ok(row_infos) } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs index 6578ceb933..ccd16a019e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs @@ -1,4 +1,4 @@ -use crate::entities::{FieldType, GridFieldPB}; +use crate::entities::{FieldPB, FieldType}; use crate::services::field::type_options::*; use bytes::Bytes; use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry}; @@ -28,7 +28,7 @@ impl FieldBuilder { Self::new(type_option_builder) } - pub fn from_field(field: GridFieldPB, type_option_builder: Box) -> Self { + pub fn from_field(field: FieldPB, type_option_builder: Box) -> Self { let field_rev = FieldRevision { id: field.id, name: field.name, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index c50042edad..74a2894680 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -192,8 +192,8 @@ impl GridRevisionEditor { pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> { let _ = self.modify(|grid_pad| Ok(grid_pad.delete_field_rev(field_id)?)).await?; - let field_order = GridFieldIdPB::from(field_id); - let notified_changeset = GridFieldChangesetPB::delete(&self.grid_id, vec![field_order]); + let field_order = FieldIdPB::from(field_id); + let notified_changeset = FieldChangesetPB::delete(&self.grid_id, vec![field_order]); let _ = self.notify_did_update_grid(notified_changeset).await?; Ok(()) } @@ -272,13 +272,13 @@ impl GridRevisionEditor { Ok(()) } - pub async fn create_row(&self, start_row_id: Option) -> FlowyResult { + pub async fn create_row(&self, start_row_id: Option) -> FlowyResult { let field_revs = self.grid_pad.read().await.get_field_revs(None)?; let block_id = self.block_id().await?; // insert empty row below the row whose id is upper_row_id let row_rev = RowRevisionBuilder::new(&block_id, &field_revs).build(); - let row_order = GridRowPB::from(&row_rev); + let row_order = RowPB::from(&row_rev); // insert the row let row_count = self.block_manager.create_row(&block_id, row_rev, start_row_id).await?; @@ -289,12 +289,12 @@ impl GridRevisionEditor { Ok(row_order) } - pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { + pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { let block_id = self.block_id().await?; let mut rows_by_block_id: HashMap> = HashMap::new(); let mut row_orders = vec![]; for row_rev in row_revs { - row_orders.push(GridRowPB::from(&row_rev)); + row_orders.push(RowPB::from(&row_rev)); rows_by_block_id .entry(block_id.clone()) .or_insert_with(Vec::new) @@ -406,7 +406,7 @@ impl GridRevisionEditor { } } - pub async fn get_blocks(&self, block_ids: Option>) -> FlowyResult { + pub async fn get_blocks(&self, block_ids: Option>) -> FlowyResult { let block_snapshots = self.grid_block_snapshots(block_ids.clone()).await?; make_grid_blocks(block_ids, block_snapshots) } @@ -416,7 +416,7 @@ impl GridRevisionEditor { Ok(block_meta_revs) } - pub async fn delete_rows(&self, row_orders: Vec) -> FlowyResult<()> { + pub async fn delete_rows(&self, row_orders: Vec) -> FlowyResult<()> { let changesets = self.block_manager.delete_rows(row_orders).await?; for changeset in changesets { let _ = self.update_block(changeset).await?; @@ -429,12 +429,12 @@ impl GridRevisionEditor { let field_orders = pad_read_guard .get_field_revs(None)? .iter() - .map(GridFieldIdPB::from) + .map(FieldIdPB::from) .collect(); let mut block_orders = vec![]; for block_rev in pad_read_guard.get_block_meta_revs() { let row_orders = self.block_manager.get_row_orders(&block_rev.block_id).await?; - let block_order = GridBlockPB { + let block_order = BlockPB { id: block_rev.block_id.clone(), rows: row_orders, }; @@ -512,9 +512,9 @@ impl GridRevisionEditor { .modify(|grid_pad| Ok(grid_pad.move_field(field_id, from as usize, to as usize)?)) .await?; if let Some((index, field_rev)) = self.grid_pad.read().await.get_field_rev(field_id) { - let delete_field_order = GridFieldIdPB::from(field_id); + let delete_field_order = FieldIdPB::from(field_id); let insert_field = IndexFieldPB::from_field_rev(field_rev, index); - let notified_changeset = GridFieldChangesetPB { + let notified_changeset = FieldChangesetPB { grid_id: self.grid_id.clone(), inserted_fields: vec![insert_field], deleted_fields: vec![delete_field_order], @@ -605,7 +605,7 @@ impl GridRevisionEditor { async fn notify_did_insert_grid_field(&self, field_id: &str) -> FlowyResult<()> { if let Some((index, field_rev)) = self.grid_pad.read().await.get_field_rev(field_id) { let index_field = IndexFieldPB::from_field_rev(field_rev, index); - let notified_changeset = GridFieldChangesetPB::insert(&self.grid_id, vec![index_field]); + let notified_changeset = FieldChangesetPB::insert(&self.grid_id, vec![index_field]); let _ = self.notify_did_update_grid(notified_changeset).await?; } Ok(()) @@ -620,8 +620,8 @@ impl GridRevisionEditor { .get_field_rev(field_id) .map(|(index, field)| (index, field.clone())) { - let updated_field = GridFieldPB::from(field_rev); - let notified_changeset = GridFieldChangesetPB::update(&self.grid_id, vec![updated_field.clone()]); + let updated_field = FieldPB::from(field_rev); + let notified_changeset = FieldChangesetPB::update(&self.grid_id, vec![updated_field.clone()]); let _ = self.notify_did_update_grid(notified_changeset).await?; send_dart_notification(field_id, GridNotification::DidUpdateField) @@ -632,7 +632,7 @@ impl GridRevisionEditor { Ok(()) } - async fn notify_did_update_grid(&self, changeset: GridFieldChangesetPB) -> FlowyResult<()> { + async fn notify_did_update_grid(&self, changeset: FieldChangesetPB) -> FlowyResult<()> { send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridField) .payload(changeset) .send(); diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs index 3a5aa58c6d..24c2cda68a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs @@ -1,4 +1,4 @@ -use crate::entities::{GridBlockPB, GridRowPB, RepeatedGridBlockPB}; +use crate::entities::{BlockPB, RepeatedBlockPB, RowPB}; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::RowRevision; use std::collections::HashMap; @@ -9,14 +9,14 @@ pub struct GridBlockSnapshot { pub row_revs: Vec>, } -pub(crate) fn block_from_row_orders(row_orders: Vec) -> Vec { - let mut map: HashMap = HashMap::new(); +pub(crate) fn block_from_row_orders(row_orders: Vec) -> Vec { + let mut map: HashMap = HashMap::new(); row_orders.into_iter().for_each(|row_info| { // Memory Optimization: escape clone block_id let block_id = row_info.block_id().to_owned(); let cloned_block_id = block_id.clone(); map.entry(block_id) - .or_insert_with(|| GridBlockPB::new(&cloned_block_id, vec![])) + .or_insert_with(|| BlockPB::new(&cloned_block_id, vec![])) .rows .push(row_info); }); @@ -35,16 +35,16 @@ pub(crate) fn block_from_row_orders(row_orders: Vec) -> Vec]) -> Vec { - row_revs.iter().map(GridRowPB::from).collect::>() +pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc]) -> Vec { + row_revs.iter().map(RowPB::from).collect::>() } -pub(crate) fn make_row_from_row_rev(row_rev: Arc) -> Option { +pub(crate) fn make_row_from_row_rev(row_rev: Arc) -> Option { make_rows_from_row_revs(&[row_rev]).pop() } -pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc]) -> Vec { - let make_row = |row_rev: &Arc| GridRowPB { +pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc]) -> Vec { + let make_row = |row_rev: &Arc| RowPB { block_id: row_rev.block_id.clone(), id: row_rev.id.clone(), height: row_rev.height, @@ -56,15 +56,15 @@ pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc]) -> Vec>, block_snapshots: Vec, -) -> FlowyResult { +) -> FlowyResult { match block_ids { None => Ok(block_snapshots .into_iter() .map(|snapshot| { let row_orders = make_row_orders_from_row_revs(&snapshot.row_revs); - GridBlockPB::new(&snapshot.block_id, row_orders) + BlockPB::new(&snapshot.block_id, row_orders) }) - .collect::>() + .collect::>() .into()), Some(block_ids) => { let block_meta_data_map: HashMap<&String, &Vec>> = block_snapshots @@ -78,7 +78,7 @@ pub(crate) fn make_grid_blocks( None => {} Some(row_revs) => { let row_orders = make_row_orders_from_row_revs(row_revs); - grid_blocks.push(GridBlockPB::new(&block_id, row_orders)); + grid_blocks.push(BlockPB::new(&block_id, row_orders)); } } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index 14671c2eb2..900a41c6ed 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -2,7 +2,7 @@ use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow}; use crate::grid::block_test::util::GridRowTestBuilder; use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::{FieldType, GridCellIdParams, GridRowPB}; +use flowy_grid::entities::{FieldType, GridCellIdParams, RowPB}; use flowy_grid::services::field::*; use flowy_grid_data_model::revision::{ GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, @@ -97,7 +97,7 @@ impl GridRowTest { let row_orders = row_ids .into_iter() .map(|row_id| self.row_order_by_row_id.get(&row_id).unwrap().clone()) - .collect::>(); + .collect::>(); self.editor.delete_rows(row_orders).await.unwrap(); self.row_revs = self.get_row_revs().await; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs index 9987b60671..bfd8c92f0a 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs @@ -17,7 +17,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { .protobuf_bytes() .to_vec(); - let field = GridFieldPB { + let field = FieldPB { id: field_rev.id, name: field_rev.name, desc: field_rev.desc, @@ -50,7 +50,7 @@ pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldRev .protobuf_bytes() .to_vec(); - let field = GridFieldPB { + let field = FieldPB { id: field_rev.id, name: field_rev.name, desc: field_rev.desc, diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index f4a1528f8f..a0f1b6d9cf 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -32,7 +32,7 @@ pub struct GridEditorTest { pub block_meta_revs: Vec>, pub row_revs: Vec>, pub field_count: usize, - pub row_order_by_row_id: HashMap, + pub row_order_by_row_id: HashMap, } impl GridEditorTest { From 8b535720ef5abc5725a0bf95d3a4ea5540f17f14 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 11 Aug 2022 13:33:36 +0800 Subject: [PATCH 068/224] chore: add AF prefix to appflowy_board classes --- .../plugins/board/application/board_bloc.dart | 8 ++-- .../application/board_data_controller.dart | 7 +--- .../board/presentation/board_page.dart | 10 ++--- .../header/type_option/multi_select.dart | 17 +++++--- .../example/lib/multi_board_list_example.dart | 14 +++---- .../lib/single_board_list_example.dart | 6 +-- .../appflowy_board/lib/src/widgets/board.dart | 40 +++++++++---------- .../widgets/board_column/board_column.dart | 36 ++++++++--------- .../board_column/board_column_data.dart | 10 ++--- .../lib/src/widgets/board_data.dart | 16 ++++---- .../src/revision/grid_setting_rev.rs | 1 + 11 files changed, 83 insertions(+), 82 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 5c2072d2f1..66eeddc76a 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -18,12 +18,12 @@ part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { final GridDataController _gridDataController; - late final BoardDataController boardDataController; + late final AFBoardDataController boardDataController; BoardBloc({required ViewPB view}) : _gridDataController = GridDataController(view: view), super(BoardState.initial(view.id)) { - boardDataController = BoardDataController( + boardDataController = AFBoardDataController( onMoveColumn: ( fromIndex, toIndex, @@ -120,8 +120,8 @@ class BoardBloc extends Bloc { typeOptionContext.loadTypeOptionData( onCompleted: (singleSelect) { - List columns = singleSelect.options.map((option) { - return BoardColumnData( + List columns = singleSelect.options.map((option) { + return AFBoardColumnData( id: option.id, desc: option.name, customData: option, diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index d52b516663..d5e72fa908 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -12,13 +12,8 @@ // typedef OnFieldsChanged = void Function(UnmodifiableListView); // typedef OnGridChanged = void Function(GridPB); -// typedef OnRowsChanged = void Function( -// List rowInfos, -// GridRowChangeReason, -// ); -// typedef ListenONRowChangedCondition = bool Function(); -// class GridDataController { +// class ridDataController { // final String gridId; // final GridService _gridFFIService; // final GridFieldCache fieldCache; diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 60b788ae6d..56a2dfa5f5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -37,7 +37,7 @@ class BoardPage extends StatelessWidget { } class BoardContent extends StatelessWidget { - final config = BoardConfig( + final config = AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); @@ -51,13 +51,13 @@ class BoardContent extends StatelessWidget { color: Colors.white, child: Padding( padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Board( + child: AFBoard( dataController: context.read().boardDataController, headerBuilder: _buildHeader, footBuilder: _buildFooter, cardBuilder: _buildCard, columnConstraints: const BoxConstraints.tightFor(width: 240), - config: BoardConfig( + config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ), ), @@ -67,7 +67,7 @@ class BoardContent extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context, BoardColumnData columnData) { + Widget _buildHeader(BuildContext context, AFBoardColumnData columnData) { return AppFlowyColumnHeader( icon: const Icon(Icons.lightbulb_circle), title: Text(columnData.desc), @@ -78,7 +78,7 @@ class BoardContent extends StatelessWidget { ); } - Widget _buildFooter(BuildContext context, BoardColumnData columnData) { + Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { return AppFlowyColumnFooter( icon: const Icon(Icons.add, size: 20), title: const Text('New'), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart index 9c0f860055..ab8a628146 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/multi_select.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/multi_select_type_option.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:flutter/material.dart'; import '../field_type_option_editor.dart'; @@ -9,10 +10,14 @@ class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final MultiSelectTypeOptionWidget _widget; MultiSelectTypeOptionWidgetBuilder( - MultiSelectAction typeOptionContext, + MultiSelectTypeOptionContext typeOptionContext, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = MultiSelectTypeOptionWidget( - typeOptionContext: typeOptionContext, + selectOptionAction: MultiSelectAction( + fieldId: typeOptionContext.fieldId, + gridId: typeOptionContext.gridId, + typeOptionContext: typeOptionContext, + ), overlayDelegate: overlayDelegate, ); @@ -21,11 +26,11 @@ class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { } class MultiSelectTypeOptionWidget extends TypeOptionWidget { - final MultiSelectAction typeOptionContext; + final MultiSelectAction selectOptionAction; final TypeOptionOverlayDelegate overlayDelegate; const MultiSelectTypeOptionWidget({ - required this.typeOptionContext, + required this.selectOptionAction, required this.overlayDelegate, Key? key, }) : super(key: key); @@ -33,10 +38,10 @@ class MultiSelectTypeOptionWidget extends TypeOptionWidget { @override Widget build(BuildContext context) { return SelectOptionTypeOptionWidget( - options: typeOptionContext.typeOption.options, + options: selectOptionAction.typeOption.options, beginEdit: () => overlayDelegate.hideOverlay(context), overlayDelegate: overlayDelegate, - typeOptionAction: typeOptionContext, + typeOptionAction: selectOptionAction, // key: ValueKey(state.typeOption.hashCode), ); } diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index d692a6fe20..eb16ba09ba 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -9,7 +9,7 @@ class MultiBoardListExample extends StatefulWidget { } class _MultiBoardListExampleState extends State { - final BoardDataController boardDataController = BoardDataController( + final AFBoardDataController boardDataController = AFBoardDataController( onMoveColumn: (fromIndex, toIndex) { debugPrint('Move column from $fromIndex to $toIndex'); }, @@ -23,18 +23,18 @@ class _MultiBoardListExampleState extends State { @override void initState() { - final column1 = BoardColumnData(id: "To Do", items: [ + final column1 = AFBoardColumnData(id: "To Do", items: [ TextItem("Card 1"), TextItem("Card 2"), RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 4"), ]); - final column2 = BoardColumnData(id: "In Progress", items: [ + final column2 = AFBoardColumnData(id: "In Progress", items: [ RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 6"), ]); - final column3 = BoardColumnData(id: "Done", items: []); + final column3 = AFBoardColumnData(id: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); @@ -45,14 +45,14 @@ class _MultiBoardListExampleState extends State { @override Widget build(BuildContext context) { - final config = BoardConfig( + final config = AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ); return Container( color: Colors.white, child: Padding( padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Board( + child: AFBoard( dataController: boardDataController, footBuilder: (context, columnData) { return AppFlowyColumnFooter( @@ -79,7 +79,7 @@ class _MultiBoardListExampleState extends State { ); }, columnConstraints: const BoxConstraints.tightFor(width: 240), - config: BoardConfig( + config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), ), ), diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart index 655f1439c1..8c4226a65c 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart @@ -9,11 +9,11 @@ class SingleBoardListExample extends StatefulWidget { } class _SingleBoardListExampleState extends State { - final BoardDataController boardData = BoardDataController(); + final AFBoardDataController boardData = AFBoardDataController(); @override void initState() { - final column = BoardColumnData(id: "1", items: [ + final column = AFBoardColumnData(id: "1", items: [ TextItem("a"), TextItem("b"), TextItem("c"), @@ -26,7 +26,7 @@ class _SingleBoardListExampleState extends State { @override Widget build(BuildContext context) { - return Board( + return AFBoard( dataController: boardData, cardBuilder: (context, item) { return _RowWidget(item: item as TextItem, key: ObjectKey(item)); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 3cd2a331f1..03d9ca1750 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -8,13 +8,13 @@ import 'reorder_flex/reorder_flex.dart'; import 'reorder_phantom/phantom_controller.dart'; import '../rendering/board_overlay.dart'; -class BoardConfig { +class AFBoardConfig { final double cornerRadius; final EdgeInsets columnPadding; final EdgeInsets columnItemPadding; final Color columnBackgroundColor; - const BoardConfig({ + const AFBoardConfig({ this.cornerRadius = 6.0, this.columnPadding = const EdgeInsets.symmetric(horizontal: 8), this.columnItemPadding = const EdgeInsets.symmetric(horizontal: 10), @@ -22,7 +22,7 @@ class BoardConfig { }); } -class Board extends StatelessWidget { +class AFBoard extends StatelessWidget { /// The direction to use as the main axis. final Axis direction = Axis.vertical; @@ -30,32 +30,32 @@ class Board extends StatelessWidget { final Widget? background; /// - final BoardColumnCardBuilder cardBuilder; + final AFBoardColumnCardBuilder cardBuilder; /// - final BoardColumnHeaderBuilder? headerBuilder; + final AFBoardColumnHeaderBuilder? headerBuilder; /// - final BoardColumnFooterBuilder? footBuilder; + final AFBoardColumnFooterBuilder? footBuilder; /// - final BoardDataController dataController; + final AFBoardDataController dataController; final BoxConstraints columnConstraints; /// final BoardPhantomController phantomController; - final BoardConfig config; + final AFBoardConfig config; - Board({ + AFBoard({ required this.dataController, required this.cardBuilder, this.background, this.footBuilder, this.headerBuilder, this.columnConstraints = const BoxConstraints(maxWidth: 200), - this.config = const BoardConfig(), + this.config = const AFBoardConfig(), Key? key, }) : phantomController = BoardPhantomController(delegate: dataController), super(key: key); @@ -64,7 +64,7 @@ class Board extends StatelessWidget { Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: dataController, - child: Consumer( + child: Consumer( builder: (context, notifier, child) { return BoardContent( config: config, @@ -89,20 +89,20 @@ class BoardContent extends StatefulWidget { final OnDragStarted? onDragStarted; final OnReorder onReorder; final OnDragEnded? onDragEnded; - final BoardDataController dataController; + final AFBoardDataController dataController; final Widget? background; - final BoardConfig config; + final AFBoardConfig config; final ReorderFlexConfig reorderFlexConfig; final BoxConstraints columnConstraints; /// - final BoardColumnCardBuilder cardBuilder; + final AFBoardColumnCardBuilder cardBuilder; /// - final BoardColumnHeaderBuilder? headerBuilder; + final AFBoardColumnHeaderBuilder? headerBuilder; /// - final BoardColumnFooterBuilder? footBuilder; + final AFBoardColumnFooterBuilder? footBuilder; final OverlapDragTargetDelegate delegate; @@ -206,7 +206,7 @@ class _BoardContentState extends State { builder: (context, value, child) { return ConstrainedBox( constraints: widget.columnConstraints, - child: BoardColumnWidget( + child: AFBoardColumnWidget( margin: _marginFromIndex(columnIndex), itemMargin: widget.config.columnItemPadding, headerBuilder: widget.headerBuilder, @@ -246,9 +246,9 @@ class _BoardContentState extends State { } } -class _BoardColumnDataSourceImpl extends BoardColumnDataDataSource { +class _BoardColumnDataSourceImpl extends AFBoardColumnDataDataSource { String columnId; - final BoardDataController dataController; + final AFBoardDataController dataController; _BoardColumnDataSourceImpl({ required this.columnId, @@ -256,7 +256,7 @@ class _BoardColumnDataSourceImpl extends BoardColumnDataDataSource { }); @override - BoardColumnData get columnData => + AFBoardColumnData get columnData => dataController.columnController(columnId).columnData; @override diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index d8981096e3..09d441bc16 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -22,23 +22,23 @@ typedef OnColumnDeleted = void Function(String listId, int deletedIndex); typedef OnColumnInserted = void Function(String listId, int insertedIndex); -typedef BoardColumnCardBuilder = Widget Function( +typedef AFBoardColumnCardBuilder = Widget Function( BuildContext context, ColumnItem item, ); -typedef BoardColumnHeaderBuilder = Widget Function( +typedef AFBoardColumnHeaderBuilder = Widget Function( BuildContext context, - BoardColumnData columnData, + AFBoardColumnData columnData, ); -typedef BoardColumnFooterBuilder = Widget Function( +typedef AFBoardColumnFooterBuilder = Widget Function( BuildContext context, - BoardColumnData columnData, + AFBoardColumnData columnData, ); -abstract class BoardColumnDataDataSource extends ReoderFlextDataSource { - BoardColumnData get columnData; +abstract class AFBoardColumnDataDataSource extends ReoderFlextDataSource { + AFBoardColumnData get columnData; List get acceptedColumnIds; @@ -49,7 +49,7 @@ abstract class BoardColumnDataDataSource extends ReoderFlextDataSource { UnmodifiableListView get items => columnData.items; void debugPrint() { - String msg = '[$BoardColumnDataDataSource] $columnData data: '; + String msg = '[$AFBoardColumnDataDataSource] $columnData data: '; for (var element in items) { msg = '$msg$element,'; } @@ -58,10 +58,10 @@ abstract class BoardColumnDataDataSource extends ReoderFlextDataSource { } } -/// [BoardColumnWidget] represents the column of the Board. +/// [AFBoardColumnWidget] represents the column of the Board. /// -class BoardColumnWidget extends StatefulWidget { - final BoardColumnDataDataSource dataSource; +class AFBoardColumnWidget extends StatefulWidget { + final AFBoardColumnDataDataSource dataSource; final ScrollController? scrollController; final ReorderFlexConfig config; @@ -73,11 +73,11 @@ class BoardColumnWidget extends StatefulWidget { String get columnId => dataSource.columnData.id; - final BoardColumnCardBuilder cardBuilder; + final AFBoardColumnCardBuilder cardBuilder; - final BoardColumnHeaderBuilder? headerBuilder; + final AFBoardColumnHeaderBuilder? headerBuilder; - final BoardColumnFooterBuilder? footBuilder; + final AFBoardColumnFooterBuilder? footBuilder; final EdgeInsets margin; @@ -87,7 +87,7 @@ class BoardColumnWidget extends StatefulWidget { final Color backgroundColor; - const BoardColumnWidget({ + const AFBoardColumnWidget({ Key? key, this.headerBuilder, this.footBuilder, @@ -106,12 +106,12 @@ class BoardColumnWidget extends StatefulWidget { super(key: key); @override - State createState() => _BoardColumnWidgetState(); + State createState() => _AFBoardColumnWidgetState(); } -class _BoardColumnWidgetState extends State { +class _AFBoardColumnWidgetState extends State { final GlobalKey _columnOverlayKey = - GlobalKey(debugLabel: '$BoardColumnWidget overlay key'); + GlobalKey(debugLabel: '$AFBoardColumnWidget overlay key'); late BoardOverlayEntry _overlayEntry; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 97551ef350..b7ef467a23 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -12,7 +12,7 @@ abstract class ColumnItem extends ReoderFlexItem { String toString() => id; } -/// [BoardColumnDataController] is used to handle the [BoardColumnData]. +/// [BoardColumnDataController] is used to handle the [AFBoardColumnData]. /// * Remove an item by calling [removeAt] method. /// * Move item to another position by calling [move] method. /// * Insert item to index by calling [insert] method @@ -21,7 +21,7 @@ abstract class ColumnItem extends ReoderFlexItem { /// All there operations will notify listeners by default. /// class BoardColumnDataController extends ChangeNotifier with EquatableMixin { - final BoardColumnData columnData; + final AFBoardColumnData columnData; BoardColumnDataController({ required this.columnData, @@ -112,15 +112,15 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { } } -/// [BoardColumnData] represents the data of each Column of the Board. -class BoardColumnData extends ReoderFlexItem with EquatableMixin { +/// [AFBoardColumnData] represents the data of each Column of the Board. +class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { @override final String id; final String desc; final List _items; final CustomData? customData; - BoardColumnData({ + AFBoardColumnData({ this.customData, required this.id, this.desc = "", diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 37bae68121..60bde5bc79 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -23,14 +23,14 @@ typedef OnMoveColumnItemToColumn = void Function( int toIndex, ); -class BoardDataController extends ChangeNotifier +class AFBoardDataController extends ChangeNotifier with EquatableMixin, BoardPhantomControllerDelegate, ReoderFlextDataSource { - final List _columnDatas = []; + final List _columnDatas = []; final OnMoveColumn? onMoveColumn; final OnMoveColumnItem? onMoveColumnItem; final OnMoveColumnItemToColumn? onMoveColumnItemToColumn; - List get columnDatas => _columnDatas; + List get columnDatas => _columnDatas; List get columnIds => _columnDatas.map((columnData) => columnData.id).toList(); @@ -38,13 +38,13 @@ class BoardDataController extends ChangeNotifier final LinkedHashMap _columnControllers = LinkedHashMap(); - BoardDataController({ + AFBoardDataController({ this.onMoveColumn, this.onMoveColumnItem, this.onMoveColumnItemToColumn, }); - void addColumn(BoardColumnData columnData, {bool notify = true}) { + void addColumn(AFBoardColumnData columnData, {bool notify = true}) { if (_columnControllers[columnData.id] != null) return; final controller = BoardColumnDataController(columnData: columnData); @@ -53,7 +53,7 @@ class BoardDataController extends ChangeNotifier if (notify) notifyListeners(); } - void addColumns(List columns, {bool notify = true}) { + void addColumns(List columns, {bool notify = true}) { for (final column in columns) { addColumn(column, notify: false); } @@ -158,7 +158,7 @@ class BoardDataController extends ChangeNotifier } @override - String get identifier => '$BoardDataController'; + String get identifier => '$AFBoardDataController'; @override UnmodifiableListView get items => @@ -175,7 +175,7 @@ class BoardDataController extends ChangeNotifier columnController.removeAt(index); Log.debug( - '[$BoardDataController] Column:[$columnId] remove phantom, current count: ${columnController.items.length}'); + '[$AFBoardDataController] Column:[$columnId] remove phantom, current count: ${columnController.items.length}'); } return isExist; } diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 153ee10961..277de36d62 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -39,6 +39,7 @@ pub struct GridSettingRevision { pub filters: GridFilters, + #[serde(default)] pub groups: GridGroups, #[serde(skip)] From 3087594b3c4cb79b5391da83070258da68f9f72a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 12:28:12 +0800 Subject: [PATCH 069/224] fix: #814 --- .../flowy_editor/example/lib/main.dart | 5 +- .../src/render/rich_text/flowy_rich_text.dart | 49 ++++++++----------- .../whitespace_handler.dart | 16 +++--- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 19c732f6a0..d33a010b55 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -63,7 +63,10 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: _buildBody(), + body: Container( + alignment: Alignment.topCenter, + child: _buildBody(), + ), floatingActionButton: _buildExpandableFab(), ); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index bdfae73d66..c77b52b25f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -44,7 +44,6 @@ class _FlowyRichTextState extends State with Selectable { final _placeholderTextKey = GlobalKey(); final _lineHeight = 1.5; - double? _cursorHeight; RenderParagraph get _renderParagraph => _textKey.currentContext?.findRenderObject() as RenderParagraph; @@ -57,13 +56,6 @@ class _FlowyRichTextState extends State with Selectable { return _buildRichText(context); } - @override - void didUpdateWidget(covariant FlowyRichText oldWidget) { - super.didUpdateWidget(oldWidget); - - _cursorHeight = null; - } - @override Position start() => Position(path: widget.textNode.path, offset: 0); @@ -76,16 +68,18 @@ class _FlowyRichTextState extends State with Selectable { final textPosition = TextPosition(offset: position.offset); final cursorOffset = _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); - _cursorHeight ??= widget.cursorHeight ?? + final cursorHeight = widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? _placeholderRenderParagraph.getFullHeightForCaret(textPosition) ?? - 18.0; // default height - return Rect.fromLTWH( + 16.0; // default height + + final rect = Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, widget.cursorWidth, - _cursorHeight!, + cursorHeight, ); + return rect; } @override @@ -148,24 +142,13 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildPlaceholderText(BuildContext context) { - final textSpan = TextSpan( - children: [ - TextSpan( - text: widget.placeholderText, - style: TextStyle( - color: widget.textNode.toRawString().isNotEmpty - ? Colors.transparent - : Colors.grey, - fontSize: baseFontSize, - height: _lineHeight, - ), - ), - ], - ); + final textSpan = _placeholderTextSpan; return RichText( key: _placeholderTextKey, - text: widget.placeholderTextSpanDecorator != null - ? widget.placeholderTextSpanDecorator!(textSpan) + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, applyHeightToLastDescent: false), + text: widget.textSpanDecorator != null + ? widget.textSpanDecorator!(textSpan) : textSpan, ); } @@ -219,4 +202,14 @@ class _FlowyRichTextState extends State with Selectable { ).toTextSpan()) .toList(growable: false), ); + + TextSpan get _placeholderTextSpan => TextSpan(children: [ + RichTextStyle( + text: widget.placeholderText, + attributes: { + StyleKey.color: '0xFF707070', + }, + height: _lineHeight, + ).toTextSpan() + ]); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index 41574e6aaa..0deb3d44d2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -39,8 +39,8 @@ FlowyKeyEventHandler whiteSpaceHandler = (editorState, event) { return _toCheckboxList(editorState, textNode); } else if (_bulletedListSymbols.any(text.startsWith)) { return _toBulletedList(editorState, textNode); - } else if (_countOfSign(text) != 0) { - return _toHeadingStyle(editorState, textNode); + } else if (_countOfSign(text, selection) != 0) { + return _toHeadingStyle(editorState, textNode, selection); } return KeyEventResult.ignored; @@ -99,8 +99,12 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) { return KeyEventResult.handled; } -KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) { - final x = _countOfSign(textNode.toRawString()); +KeyEventResult _toHeadingStyle( + EditorState editorState, TextNode textNode, Selection selection) { + final x = _countOfSign( + textNode.toRawString(), + selection, + ); final hX = 'h$x'; if (textNode.attributes.heading == hX) { return KeyEventResult.ignored; @@ -121,9 +125,9 @@ KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) { return KeyEventResult.handled; } -int _countOfSign(String text) { +int _countOfSign(String text, Selection selection) { for (var i = 6; i >= 0; i--) { - if (text.startsWith('#' * i)) { + if (text.substring(0, selection.end.offset).startsWith('#' * i)) { return i; } } From afe11a4f914b78f1962deed49326e65fd917f485 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 11 Aug 2022 13:47:49 +0800 Subject: [PATCH 070/224] fix: selection state mismatch --- .../flowy_editor/lib/src/editor_state.dart | 12 ++++++---- .../lib/src/service/selection_service.dart | 23 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 82725b5255..31a321a3c0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -21,6 +21,11 @@ class ApplyOptions { }); } +enum CursorUpdateReason { + uiEvent, + others, +} + class EditorState { final StateTree document; @@ -37,11 +42,10 @@ class EditorState { } /// add the set reason in the future, don't use setter - updateCursorSelection(Selection? cursorSelection) { + updateCursorSelection(Selection? cursorSelection, + [CursorUpdateReason reason = CursorUpdateReason.others]) { // broadcast to other users here - if (cursorSelection == null) { - service.selectionService.clearSelection(); - } else { + if (reason != CursorUpdateReason.uiEvent) { service.selectionService.updateSelection(cursorSelection); } _cursorSelection = cursorSelection; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index 4a4fd6002c..552e9eaf69 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -52,7 +52,7 @@ abstract class FlowySelectionService { /// The editor will update selection area and toolbar area /// if the [selection] is not collapsed, /// otherwise, will update the cursor area. - void updateSelection(Selection selection); + void updateSelection(Selection? selection); /// Clears the selection area, cursor area and the popup list area. void clearSelection(); @@ -180,21 +180,24 @@ class _FlowySelectionState extends State } @override - void updateSelection(Selection selection) { + void updateSelection(Selection? selection) { selectionRects.clear(); clearSelection(); - if (selection.isCollapsed) { - /// updates cursor area. - debugPrint('updating cursor'); - _updateCursorAreas(selection.start); - } else { - // updates selection area. - debugPrint('updating selection'); - _updateSelectionAreas(selection); + if (selection != null) { + if (selection.isCollapsed) { + /// updates cursor area. + debugPrint('updating cursor'); + _updateCursorAreas(selection.start); + } else { + // updates selection area. + debugPrint('updating selection'); + _updateSelectionAreas(selection); + } } currentSelection.value = selection; + editorState.updateCursorSelection(selection, CursorUpdateReason.uiEvent); } @override From cefd571dd008e20e8ee49d6c9742afe0fa1eda3c Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 11 Aug 2022 15:00:36 +0800 Subject: [PATCH 071/224] chore: add board data controller --- .../plugins/board/application/board_bloc.dart | 32 ++- .../application/board_data_controller.dart | 206 +++++++++--------- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../lib/plugins/board/presentation/card.dart | 2 +- .../grid/application/field/field_service.dart | 3 - .../select_option_type_option_bloc.dart | 2 +- .../application/grid_data_controller.dart | 2 +- .../grid/application/grid_service.dart | 6 + .../widgets/header/type_option/builder.dart | 2 - .../src/services/view/controller.rs | 2 +- .../flowy-folder/tests/workspace/script.rs | 1 + .../src/entities/group_entities/group.rs | 2 +- .../src/entities/setting_entities.rs | 2 +- .../flowy-grid/src/services/grid_editor.rs | 2 + .../src/services/group/group_service.rs | 3 + .../flowy-grid/src/services/group/mod.rs | 2 +- .../src/services/row/row_builder.rs | 4 +- frontend/rust-lib/flowy-grid/src/util.rs | 15 +- .../flowy-grid/tests/grid/block_test/util.rs | 2 - 19 files changed, 141 insertions(+), 151 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 66eeddc76a..581387e5ac 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; -import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -14,14 +13,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:collection'; +import 'board_data_controller.dart'; + part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { - final GridDataController _gridDataController; + final BoardDataController _dataController; late final AFBoardDataController boardDataController; BoardBloc({required ViewPB view}) - : _gridDataController = GridDataController(view: view), + : _dataController = BoardDataController(view: view), super(BoardState.initial(view.id)) { boardDataController = AFBoardDataController( onMoveColumn: ( @@ -51,7 +52,7 @@ class BoardBloc extends Bloc { await _loadGrid(emit); }, createRow: () { - _gridDataController.createRow(); + _dataController.createRow(); }, didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); @@ -66,39 +67,34 @@ class BoardBloc extends Bloc { @override Future close() async { - await _gridDataController.dispose(); + await _dataController.dispose(); return super.close(); } GridRowCache? getRowCache(String blockId, String rowId) { - final GridBlockCache? blockCache = _gridDataController.blocks[blockId]; + final GridBlockCache? blockCache = _dataController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - _gridDataController.addListener( + _dataController.addListener( onGridChanged: (grid) { if (!isClosed) { add(BoardEvent.didReceiveGridUpdate(grid)); } }, - onRowsChanged: (rowInfos, reason) { - if (!isClosed) { - _buildColumnItems(rowInfos); - } - }, onFieldsChanged: (fields) { if (!isClosed) { _buildColumns(fields); } }, + onGroupChanged: (groups) {}, + onError: (err) { + Log.error(err); + }, ); } - void _buildColumnItems(List rowInfos) { - for (final rowInfo in rowInfos) {} - } - void _buildColumns(UnmodifiableListView fields) { FieldPB? groupField; for (final field in fields) { @@ -114,7 +110,7 @@ class BoardBloc extends Bloc { void _buildColumnsFromSingleSelect(FieldPB field) { final typeOptionContext = makeTypeOptionContext( - gridId: _gridDataController.gridId, + gridId: _dataController.gridId, field: field, ); @@ -135,7 +131,7 @@ class BoardBloc extends Bloc { } Future _loadGrid(Emitter emit) async { - final result = await _gridDataController.loadData(); + final result = await _dataController.loadData(); result.fold( (grid) => emit( state.copyWith(loadingState: GridLoadingState.finish(left(unit))), diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index d5e72fa908..c8e20b6172 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -1,121 +1,113 @@ -// import 'dart:collection'; +import 'dart:collection'; -// import 'package:flowy_sdk/log.dart'; -// import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -// import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; -// import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; -// import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -// import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; -// import 'dart:async'; -// import 'package:dartz/dartz.dart'; +import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:app_flowy/plugins/grid/application/grid_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'dart:async'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; -// typedef OnFieldsChanged = void Function(UnmodifiableListView); -// typedef OnGridChanged = void Function(GridPB); +typedef OnFieldsChanged = void Function(UnmodifiableListView); +typedef OnGridChanged = void Function(GridPB); +typedef OnGroupChanged = void Function(List); +typedef OnError = void Function(FlowyError); +class BoardDataController { + final String gridId; + final GridService _gridFFIService; + final GridFieldCache fieldCache; -// class ridDataController { -// final String gridId; -// final GridService _gridFFIService; -// final GridFieldCache fieldCache; + // key: the block id + final LinkedHashMap _blocks; + UnmodifiableMapView get blocks => + UnmodifiableMapView(_blocks); -// // key: the block id -// final LinkedHashMap _blocks; -// UnmodifiableMapView get blocks => -// UnmodifiableMapView(_blocks); + OnFieldsChanged? _onFieldsChanged; + OnGridChanged? _onGridChanged; + OnGroupChanged? _onGroupChanged; + OnError? _onError; -// OnRowsChanged? _onRowChanged; -// OnFieldsChanged? _onFieldsChanged; -// OnGridChanged? _onGridChanged; + BoardDataController({required ViewPB view}) + : gridId = view.id, + _blocks = LinkedHashMap.identity(), + _gridFFIService = GridService(gridId: view.id), + fieldCache = GridFieldCache(gridId: view.id); -// List get rowInfos { -// final List rows = []; -// for (var block in _blocks.values) { -// rows.addAll(block.rows); -// } -// return rows; -// } + void addListener({ + OnGridChanged? onGridChanged, + OnFieldsChanged? onFieldsChanged, + OnGroupChanged? onGroupChanged, + OnError? onError, + }) { + _onGridChanged = onGridChanged; + _onFieldsChanged = onFieldsChanged; + _onGroupChanged = onGroupChanged; + _onError = onError; -// GridDataController({required ViewPB view}) -// : gridId = view.id, -// _blocks = LinkedHashMap.identity(), -// _gridFFIService = GridService(gridId: view.id), -// fieldCache = GridFieldCache(gridId: view.id); + fieldCache.addListener(onFields: (fields) { + _onFieldsChanged?.call(UnmodifiableListView(fields)); + }); + } -// void addListener({ -// required OnGridChanged onGridChanged, -// required OnRowsChanged onRowsChanged, -// required OnFieldsChanged onFieldsChanged, -// }) { -// _onGridChanged = onGridChanged; -// _onRowChanged = onRowsChanged; -// _onFieldsChanged = onFieldsChanged; + Future> loadData() async { + final result = await _gridFFIService.loadGrid(); + return Future( + () => result.fold( + (grid) async { + _onGridChanged?.call(grid); + return await _loadFields(grid).then((result) { + return result.fold( + (l) { + _loadGroups(); + return left(l); + }, + (err) => right(err), + ); + }); + }, + (err) => right(err), + ), + ); + } -// fieldCache.addListener(onFields: (fields) { -// _onFieldsChanged?.call(UnmodifiableListView(fields)); -// }); -// } + void createRow() { + _gridFFIService.createRow(); + } -// Future> loadData() async { -// final result = await _gridFFIService.loadGrid(); -// return Future( -// () => result.fold( -// (grid) async { -// _initialBlocks(grid.blocks); -// _onGridChanged?.call(grid); -// return await _loadFields(grid); -// }, -// (err) => right(err), -// ), -// ); -// } + Future dispose() async { + await _gridFFIService.closeGrid(); + await fieldCache.dispose(); -// void createRow() { -// _gridFFIService.createRow(); -// } + for (final blockCache in _blocks.values) { + blockCache.dispose(); + } + } -// Future dispose() async { -// await _gridFFIService.closeGrid(); -// await fieldCache.dispose(); + Future> _loadFields(GridPB grid) async { + final result = await _gridFFIService.getFields(fieldIds: grid.fields); + return Future( + () => result.fold( + (fields) { + fieldCache.fields = fields.items; + _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields)); + return left(unit); + }, + (err) => right(err), + ), + ); + } -// for (final blockCache in _blocks.values) { -// blockCache.dispose(); -// } -// } - -// void _initialBlocks(List blocks) { -// for (final block in blocks) { -// if (_blocks[block.id] != null) { -// Log.warn("Initial duplicate block's cache: ${block.id}"); -// return; -// } - -// final cache = GridBlockCache( -// gridId: gridId, -// block: block, -// fieldCache: fieldCache, -// ); - -// cache.addListener( -// onChangeReason: (reason) { -// _onRowChanged?.call(rowInfos, reason); -// }, -// ); - -// _blocks[block.id] = cache; -// } -// } - -// Future> _loadFields(GridPB grid) async { -// final result = await _gridFFIService.getFields(fieldIds: grid.fields); -// return Future( -// () => result.fold( -// (fields) { -// fieldCache.fields = fields.items; -// _onFieldsChanged?.call(UnmodifiableListView(fieldCache.fields)); -// return left(unit); -// }, -// (err) => right(err), -// ), -// ); -// } -// } + Future _loadGroups() async { + final result = await _gridFFIService.loadGroups(); + return Future( + () => result.fold( + (groups) { + _onGroupChanged?.call(groups.items); + }, + (err) => _onError?.call(err), + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 36d181ae3e..2954a7cbf9 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card.dart index 0511b96b03..1410828eea 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card.dart @@ -8,6 +8,6 @@ class BoardCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Container(child: Text('1234')); + return const Text('1234'); } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index aca65dbc80..12e0f90b01 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -1,13 +1,10 @@ import 'package:dartz/dartz.dart'; -import 'package:flowy_infra/notifier.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; -import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; part 'field_service.freezed.dart'; /// FieldService consists of lots of event functions. We define the events in the backend(Rust), diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart index ef7fe53de3..d1cdb3b2ec 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/select_option_type_option_bloc.dart @@ -75,7 +75,7 @@ class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState { required List options, required bool isEditingOption, required Option newOptionName, - }) = _SelectOptionTyepOptionState; + }) = _SelectOptionTypeOptionState; factory SelectOptionTypeOptionState.initial(List options) => SelectOptionTypeOptionState( diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index dcbc67e530..32488599ea 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -20,7 +20,7 @@ typedef OnRowsChanged = void Function( List rowInfos, RowChangeReason, ); -typedef ListenONRowChangedCondition = bool Function(); +typedef ListenOnRowChangedCondition = bool Function(); class GridDataController { final String gridId; diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index 5f95827d7e..093782d32f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -5,6 +5,7 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; class GridService { @@ -38,4 +39,9 @@ class GridService { final request = ViewIdPB(value: gridId); return FolderEventCloseView(request).send(); } + + Future> loadGroups() { + final payload = GridIdPB(value: gridId); + return GridEventGetGroup(payload).send(); + } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 464c2f847c..77b4981939 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -1,6 +1,5 @@ import 'dart:typed_data'; -import 'package:app_flowy/plugins/grid/application/field/type_option/multi_select_type_option.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_data_controller.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; @@ -11,7 +10,6 @@ import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart' import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:protobuf/protobuf.dart'; -import 'package:app_flowy/plugins/grid/application/prelude.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'checkbox.dart'; diff --git a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs index defbe664db..4284d34b7c 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs @@ -1,5 +1,5 @@ pub use crate::entities::view::ViewDataTypePB; -use crate::entities::{SubViewDataTypePB, ViewInfoPB}; +use crate::entities::ViewInfoPB; use crate::manager::{ViewDataProcessor, ViewDataProcessorMap}; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index 91fac9869e..3292d6deea 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -359,6 +359,7 @@ pub async fn create_view( desc: desc.to_string(), thumbnail: None, data_type, + sub_data_type: None, plugin_type: 0, data: vec![], }; diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index 11f338de63..c27b4aaa93 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -32,7 +32,7 @@ impl std::convert::From<&GridGroupRevision> for GridGroupConfigurationPB { #[derive(ProtoBuf, Debug, Default, Clone)] pub struct RepeatedGridGroupPB { #[pb(index = 1)] - groups: Vec, + items: Vec, } #[derive(ProtoBuf, Debug, Default, Clone)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 4034e838aa..ae8037a383 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -6,7 +6,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::GridLayoutRevision; -use flowy_sync::entities::grid::{DeleteGroupParams, GridSettingChangesetParams}; +use flowy_sync::entities::grid::GridSettingChangesetParams; use std::collections::HashMap; use std::convert::TryInto; use strum::IntoEnumIterator; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 74a2894680..4654f8722c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -35,6 +35,8 @@ pub struct GridRevisionEditor { block_manager: Arc, #[allow(dead_code)] pub(crate) filter_service: Arc, + + #[allow(dead_code)] pub(crate) group_service: Arc, } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 8bc8216cf9..50d04febf4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -5,8 +5,11 @@ use std::sync::Arc; use tokio::sync::RwLock; pub(crate) struct GridGroupService { + #[allow(dead_code)] scheduler: Arc, + #[allow(dead_code)] grid_pad: Arc>, + #[allow(dead_code)] block_manager: Arc, } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index c1f767b62e..9fe58d6fc1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,3 +1,3 @@ mod group_service; -pub use group_service::*; +pub(crate) use group_service::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index 3973dd4aed..ad294e74dc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -50,18 +50,16 @@ impl<'a> RowRevisionBuilder<'a> { } } - pub fn insert_select_option_cell(mut self, field_id: &str, data: String) -> Self { + pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) { match self.field_rev_map.get(&field_id.to_owned()) { None => { tracing::warn!("Invalid field_id: {}", field_id); - self } Some(field_rev) => { let cell_data = SelectOptionCellChangeset::from_insert(&data).to_str(); let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); let cell = CellRevision::new(data); self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); - self } } } diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 7f2d3e48ab..7897dbfdaa 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -48,19 +48,18 @@ pub fn make_default_board() -> BuildGridContext { let done_option = SelectOptionPB::new("Done"); let single_select = SingleSelectTypeOptionBuilder::default() .add_option(not_started_option.clone()) - .add_option(in_progress_option.clone()) - .add_option(done_option.clone()); + .add_option(in_progress_option) + .add_option(done_option); let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); let single_select_field_id = single_select_field.id.clone(); grid_builder.add_field(single_select_field); - // rows + // Insert rows for _ in 0..3 { - grid_builder.add_row( - RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()) - .insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()) - .build(), - ); + let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); + row_builder.insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()); + let row = row_builder.build(); + grid_builder.add_row(row); } grid_builder.build() diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index ddbf5c30fe..9b97afbf56 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -10,7 +10,6 @@ use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; use strum::EnumCount; pub struct GridRowTestBuilder<'a> { - block_id: String, field_revs: &'a [Arc], inner_builder: RowRevisionBuilder<'a>, } @@ -20,7 +19,6 @@ impl<'a> GridRowTestBuilder<'a> { assert_eq!(field_revs.len(), FieldType::COUNT); let inner_builder = RowRevisionBuilder::new(block_id, field_revs); Self { - block_id: block_id.to_owned(), field_revs, inner_builder, } From 8f6eb5a0d6732251dacbc0ed7ad8b1b8f623bf4c Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 11 Aug 2022 15:25:42 +0800 Subject: [PATCH 072/224] fix: revert delta of bold --- .../lib/src/document/attributes.dart | 2 +- .../flowy_editor/test/delta_test.dart | 20 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart index 4e1f39775f..39a5f4e2ff 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart @@ -15,7 +15,7 @@ Attributes invertAttributes(Attributes? attr, Attributes? base) { return memo; }); return attr.keys.fold(baseInverted, (memo, key) { - if (attr![key] != base![key] && base.containsKey(key)) { + if (attr![key] != base![key] && !base.containsKey(key)) { memo[key] = null; } return memo; diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 3114de8dd7..48182912a3 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -230,14 +230,18 @@ void main() { expect(expected, inverted); expect(base.compose(delta).compose(inverted), base); }); - // test('retain', () { - // final delta = Delta().retain(2).retain(3, {'bold': true}); - // final base = Delta().insert('123456'); - // final expected = Delta().retain(2).retain(3, {'bold': null}); - // final inverted = delta.invert(base); - // expect(expected, inverted); - // expect(base.compose(delta).compose(inverted), base); - // }); + test('retain', () { + final delta = Delta() + ..retain(2) + ..retain(3, {'bold': true}); + final base = Delta()..insert('123456'); + final expected = Delta() + ..retain(2) + ..retain(3, {'bold': null}); + final inverted = delta.invert(base); + expect(expected, inverted); + expect(base.compose(delta).compose(inverted), base); + }); }); group('json', () { test('toJson()', () { From f4fa185976b508c4a16e1907c81026212790ba91 Mon Sep 17 00:00:00 2001 From: chiragkr04 Date: Thu, 11 Aug 2022 10:54:32 +0530 Subject: [PATCH 073/224] fix: closing rename dialog on escape key press --- .../lib/widget/dialog/styled_dialogs.dart | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index 52348dab00..78aa3d0b7f 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -3,12 +3,29 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/widget/dialog/dialog_size.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'dart:ui'; extension IntoDialog on Widget { Future show(BuildContext context) async { - await Dialogs.show(this, context); + FocusNode dialogFocusNode = FocusNode(); + await Dialogs.show( + RawKeyboardListener( + focusNode: dialogFocusNode, + onKey: (value) { + if (value.isKeyPressed(LogicalKeyboardKey.escape)) { + Navigator.of(context).pop(); + } + }, + child: this, + ), + context, + ).then( + (value) { + dialogFocusNode.dispose(); + }, + ); } } @@ -45,7 +62,8 @@ class StyledDialog extends StatelessWidget { ); if (shrinkWrap) { - innerContent = IntrinsicWidth(child: IntrinsicHeight(child: innerContent)); + innerContent = + IntrinsicWidth(child: IntrinsicHeight(child: innerContent)); } return FocusTraversalGroup( @@ -80,7 +98,8 @@ class Dialogs { return await Navigator.of(context).push( StyledDialogRoute( barrier: DialogBarrier(color: Colors.black.withOpacity(0.4)), - pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { + pageBuilder: (BuildContext buildContext, Animation animation, + Animation secondaryAnimation) { return SafeArea(child: child); }, ), @@ -132,7 +151,8 @@ class StyledDialogRoute extends PopupRoute { final RouteTransitionsBuilder? _transitionBuilder; @override - Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { return Semantics( child: _pageBuilder(context, animation, secondaryAnimation), scopesRoute: true, @@ -141,10 +161,12 @@ class StyledDialogRoute extends PopupRoute { } @override - Widget buildTransitions( - BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { if (_transitionBuilder == null) { - return FadeTransition(opacity: CurvedAnimation(parent: animation, curve: Curves.easeInOut), child: child); + return FadeTransition( + opacity: CurvedAnimation(parent: animation, curve: Curves.easeInOut), + child: child); } else { return _transitionBuilder!(context, animation, secondaryAnimation, child); } // Some default transition From 4794203e7448d8ca852af492da45c7b74e5e5372 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 11 Aug 2022 16:09:45 +0800 Subject: [PATCH 074/224] feat: handle delete keys --- .../delete_text_handler.dart | 105 ++++++++++++++---- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 5e2fdfd453..5e96e75a99 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -2,14 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/src/service/keyboard_service.dart'; - -// Handle delete text. -FlowyKeyEventHandler deleteTextHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.backspace) { - return KeyEventResult.ignored; - } +KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { final selection = editorState.service.selectionService.currentSelection.value; if (selection == null) { return KeyEventResult.ignored; @@ -22,7 +16,7 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { return KeyEventResult.ignored; } - TransactionBuilder transactionBuilder = TransactionBuilder(editorState); + final transactionBuilder = TransactionBuilder(editorState); if (textNodes.length == 1) { final textNode = textNodes.first; final index = textNode.delta.prevRunePosition(selection.start.offset); @@ -74,23 +68,90 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { } } } else { - final first = textNodes.first; - final last = textNodes.last; - var content = textNodes.last.toRawString(); - content = content.substring(selection.end.offset, content.length); - // Merge the fist and the last text node content, - // and delete the all nodes expect for the first. - transactionBuilder - ..deleteNodes(textNodes.sublist(1)) - ..mergeText( - first, - last, - firstOffset: selection.start.offset, - secondOffset: selection.end.offset, - ); + _deleteNodes(transactionBuilder, textNodes, selection); } transactionBuilder.commit(); return KeyEventResult.handled; +} + +KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { + final selection = editorState.service.selectionService.currentSelection.value; + if (selection == null) { + return KeyEventResult.ignored; + } + final nodes = editorState.service.selectionService.currentSelectedNodes; + // make sure all nodes is [TextNode]. + final textNodes = nodes.whereType().toList(); + if (textNodes.length != nodes.length) { + return KeyEventResult.ignored; + } + + final transactionBuilder = TransactionBuilder(editorState); + if (textNodes.length == 1) { + final textNode = textNodes.first; + if (selection.start.offset >= textNode.delta.length) { + debugPrint("merge next line"); + final nextNode = textNode.next; + if (nextNode == null) { + return KeyEventResult.ignored; + } + if (nextNode is TextNode) { + transactionBuilder.mergeText(textNode, nextNode); + } + transactionBuilder.deleteNode(nextNode); + } else { + final index = textNode.delta.nextRunePosition(selection.start.offset); + if (selection.isCollapsed) { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + index - selection.start.offset, + ); + } else { + transactionBuilder.deleteText( + textNode, + selection.start.offset, + selection.end.offset - selection.start.offset, + ); + } + } + } else { + _deleteNodes(transactionBuilder, textNodes, selection); + } + + transactionBuilder.commit(); + + return KeyEventResult.handled; +} + +void _deleteNodes(TransactionBuilder transactionBuilder, + List textNodes, Selection selection) { + final first = textNodes.first; + final last = textNodes.last; + var content = textNodes.last.toRawString(); + content = content.substring(selection.end.offset, content.length); + // Merge the fist and the last text node content, + // and delete the all nodes expect for the first. + transactionBuilder + ..deleteNodes(textNodes.sublist(1)) + ..mergeText( + first, + last, + firstOffset: selection.start.offset, + secondOffset: selection.end.offset, + ); +} + +// Handle delete text. +FlowyKeyEventHandler deleteTextHandler = (editorState, event) { + if (event.logicalKey == LogicalKeyboardKey.backspace) { + return _handleBackspace(editorState, event); + } + if (event.logicalKey == LogicalKeyboardKey.delete) { + return _handleDelete(editorState, event); + } + + return KeyEventResult.ignored; }; From c42f9001c4007bb865f9a1b3d0a86df504df3c1a Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 11 Aug 2022 16:40:07 +0800 Subject: [PATCH 075/224] feat: select all --- .../copy_paste_handler.dart | 1 - .../default_key_event_handlers.dart | 2 ++ .../select_all_handler.dart | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 0b87fc0fd4..a5f392a4eb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -1,5 +1,4 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/src/service/keyboard_service.dart'; import 'package:flowy_editor/src/infra/html_converter.dart'; import 'package:flowy_editor/src/document/node_iterator.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index d03211461c..f99caab04a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -7,6 +7,7 @@ import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_h import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; List defaultKeyEventHandlers = [ @@ -19,4 +20,5 @@ List defaultKeyEventHandlers = [ enterWithoutShiftInTextNodesHandler, updateTextStyleByCommandXHandler, whiteSpaceHandler, + selectAllHandler, ]; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart new file mode 100644 index 0000000000..f99569218c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/select_all_handler.dart @@ -0,0 +1,26 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +KeyEventResult _selectAll(EditorState editorState) { + if (editorState.document.root.children.isEmpty) { + return KeyEventResult.handled; + } + final firstNode = editorState.document.root.children.first; + final lastNode = editorState.document.root.children.last; + var offset = 0; + if (lastNode is TextNode) { + offset = lastNode.delta.length; + } + editorState.updateCursorSelection(Selection( + start: Position(path: firstNode.path, offset: 0), + end: Position(path: lastNode.path, offset: offset))); + return KeyEventResult.handled; +} + +FlowyKeyEventHandler selectAllHandler = (editorState, event) { + if (event.isMetaPressed && event.logicalKey == LogicalKeyboardKey.keyA) { + return _selectAll(editorState); + } + return KeyEventResult.ignored; +}; From 19838227d9292b7ec38b480a38da002c996cc8e4 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:02:04 +0800 Subject: [PATCH 076/224] feat: #818 improve user experience of the slash command --- .../flowy_editor/lib/src/document/node.dart | 5 +- .../format_rich_text_style.dart | 55 +++++++ .../slash_handler.dart | 155 ++++++++++++++---- 3 files changed, 183 insertions(+), 32 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 0b6b941aaa..97571663b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -1,7 +1,6 @@ import 'dart:collection'; import 'package:flowy_editor/src/document/path.dart'; import 'package:flowy_editor/src/document/text_delta.dart'; -import 'package:flowy_editor/src/operation/operation.dart'; import 'package:flutter/material.dart'; import './attributes.dart'; @@ -182,12 +181,12 @@ class TextNode extends Node { }) : _delta = delta, super(children: children ?? LinkedList(), attributes: attributes ?? {}); - TextNode.empty() + TextNode.empty({Attributes? attributes}) : _delta = Delta([TextInsert('')]), super( type: 'text', children: LinkedList(), - attributes: {}, + attributes: attributes ?? {}, ); Delta get delta { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index 3dcc519274..6830dd62e4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -4,9 +4,64 @@ import 'package:flowy_editor/src/document/position.dart'; import 'package:flowy_editor/src/document/selection.dart'; import 'package:flowy_editor/src/editor_state.dart'; import 'package:flowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flowy_editor/src/extensions/path_extensions.dart'; import 'package:flowy_editor/src/operation/transaction_builder.dart'; import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +void insertHeadingAfterSelection(EditorState editorState, String heading) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: heading, + }); +} + +void insertQuoteAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.quote, + }); +} + +void insertCheckboxAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.checkbox, + StyleKey.checkbox: false, + }); +} + +void insertBulletedListAfterSelection(EditorState editorState) { + insertTextNodeAfterSelection(editorState, { + StyleKey.subtype: StyleKey.bulletedList, + }); +} + +bool insertTextNodeAfterSelection( + EditorState editorState, Attributes attributes) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + if (selection == null || nodes.isEmpty) { + return false; + } + + final node = nodes.first; + if (node is TextNode && node.delta.length == 0) { + formatTextNodes(editorState, attributes); + } else { + final next = selection.end.path.next; + final builder = TransactionBuilder(editorState); + builder + ..insertNode( + next, + TextNode.empty(attributes: attributes), + ) + ..afterSelection = Selection.collapsed( + Position(path: next, offset: 0), + ) + ..commit(); + } + + return true; +} + void formatText(EditorState editorState) { formatTextNodes(editorState, {}); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 6a265808fe..fd0df50fb8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -14,43 +14,56 @@ import 'package:flutter/services.dart'; final List _popupListItems = [ PopupListItem( text: 'Text', + keywords: ['text'], icon: _popupListIcon('text'), - handler: (editorState) => formatText(editorState), + handler: (editorState) { + insertTextNodeAfterSelection(editorState, {}); + }, ), PopupListItem( text: 'Heading 1', + keywords: ['h1', 'heading 1'], icon: _popupListIcon('h1'), - handler: (editorState) => formatHeading(editorState, StyleKey.h1), + handler: (editorState) => + insertHeadingAfterSelection(editorState, StyleKey.h1), ), PopupListItem( text: 'Heading 2', + keywords: ['h2', 'heading 2'], icon: _popupListIcon('h2'), - handler: (editorState) => formatHeading(editorState, StyleKey.h2), + handler: (editorState) => + insertHeadingAfterSelection(editorState, StyleKey.h2), ), PopupListItem( text: 'Heading 3', + keywords: ['h3', 'heading 3'], icon: _popupListIcon('h3'), - handler: (editorState) => formatHeading(editorState, StyleKey.h3), + handler: (editorState) => + insertHeadingAfterSelection(editorState, StyleKey.h3), ), PopupListItem( - text: 'Bullets', + text: 'Bulleted List', + keywords: ['bulleted list'], icon: _popupListIcon('bullets'), - handler: (editorState) => formatBulletedList(editorState), + handler: (editorState) => insertBulletedListAfterSelection(editorState), ), PopupListItem( text: 'Numbered list', + keywords: ['numbered list'], icon: _popupListIcon('number'), handler: (editorState) => debugPrint('Not implement yet!'), ), PopupListItem( text: 'Checkboxes', + keywords: ['checkbox'], icon: _popupListIcon('checkbox'), - handler: (editorState) => formatCheckbox(editorState), + handler: (editorState) => insertCheckboxAfterSelection(editorState), ), ]; OverlayEntry? _popupListOverlay; EditorState? _editorState; +bool _selectionChangeBySlash = false; FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.slash) { return KeyEventResult.ignored; @@ -78,7 +91,7 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { TransactionBuilder(editorState) ..replaceText(textNode, selection.start.offset, - selection.end.offset - selection.start.offset, '/') + selection.end.offset - selection.start.offset, event.character ?? '') ..commit(); _editorState = editorState; @@ -94,7 +107,7 @@ void showPopupList( _popupListOverlay?.remove(); _popupListOverlay = OverlayEntry( builder: (context) => Positioned( - top: offset.dy + 15.0, + top: offset.dy + 20.0, left: offset.dx + 5.0, child: PopupListWidget( editorState: editorState, @@ -117,6 +130,15 @@ void clearPopupList() { if (_popupListOverlay == null || _editorState == null) { return; } + final selection = + _editorState?.service.selectionService.currentSelection.value; + if (selection == null) { + return; + } + if (_selectionChangeBySlash) { + _selectionChangeBySlash = false; + return; + } _popupListOverlay?.remove(); _popupListOverlay = null; @@ -142,21 +164,35 @@ class PopupListWidget extends StatefulWidget { } class _PopupListWidgetState extends State { - final focusNode = FocusNode(debugLabel: 'popup_list_widget'); - var selectedIndex = 0; + final _focusNode = FocusNode(debugLabel: 'popup_list_widget'); + int _selectedIndex = 0; + List _items = []; + String __keyword = ''; + String get _keyword => __keyword; + set _keyword(String keyword) { + __keyword = keyword; + setState(() { + _items = widget.items + .where((item) => + item.keywords.any((keyword) => keyword.contains(_keyword))) + .toList(growable: false); + }); + } @override void initState() { super.initState(); + _items = widget.items; + WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); + _focusNode.requestFocus(); }); } @override void dispose() { - focusNode.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -164,7 +200,7 @@ class _PopupListWidgetState extends State { @override Widget build(BuildContext context) { return Focus( - focusNode: focusNode, + focusNode: _focusNode, onKey: _onKey, child: Container( decoration: BoxDecoration( @@ -178,10 +214,25 @@ class _PopupListWidgetState extends State { ], borderRadius: BorderRadius.circular(6.0), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildColumns(widget.items, selectedIndex), - ), + child: _items.isEmpty + ? Align( + alignment: Alignment.centerLeft, + child: _buildNoResultsWidget(context), + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumns(_items, _selectedIndex), + ), + ), + ); + } + + Widget _buildNoResultsWidget(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'No results', + style: TextStyle(color: Colors.grey, fontSize: 15.0), ), ); } @@ -214,26 +265,52 @@ class _PopupListWidgetState extends State { } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + debugPrint('slash on key $event'); if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } + final arrowKeys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown + ]; + if (event.logicalKey == LogicalKeyboardKey.enter) { - if (0 <= selectedIndex && selectedIndex < widget.items.length) { - _deleteSlash(); - widget.items[selectedIndex].handler(widget.editorState); + if (0 <= _selectedIndex && _selectedIndex < _items.length) { + _deleteLastCharacters(length: _keyword.length + 1); + _items[_selectedIndex].handler(widget.editorState); return KeyEventResult.handled; } } else if (event.logicalKey == LogicalKeyboardKey.escape) { clearPopupList(); return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - clearPopupList(); - _deleteSlash(); + if (_keyword.isEmpty) { + clearPopupList(); + } else { + _keyword = _keyword.substring(0, _keyword.length - 1); + } + _deleteLastCharacters(); + return KeyEventResult.handled; + } else if (event.character != null && + !arrowKeys.contains(event.logicalKey)) { + _keyword += event.character!; + _insertText(event.character!); + var maxKeywordLength = 0; + for (final item in _items) { + for (final keyword in item.keywords) { + maxKeywordLength = max(keyword.length, maxKeywordLength); + } + } + if (_keyword.length >= maxKeywordLength + 2) { + clearPopupList(); + } return KeyEventResult.handled; } - var newSelectedIndex = selectedIndex; + var newSelectedIndex = _selectedIndex; if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { newSelectedIndex -= widget.maxItemInRow; } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { @@ -243,26 +320,44 @@ class _PopupListWidgetState extends State { } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { newSelectedIndex += 1; } - if (newSelectedIndex != selectedIndex) { + if (newSelectedIndex != _selectedIndex) { setState(() { - selectedIndex = max(0, min(widget.items.length - 1, newSelectedIndex)); + _selectedIndex = max(0, min(_items.length - 1, newSelectedIndex)); }); return KeyEventResult.handled; } return KeyEventResult.ignored; } - void _deleteSlash() { + void _deleteLastCharacters({int length = 1}) { final selection = widget.editorState.service.selectionService.currentSelection.value; final nodes = widget.editorState.service.selectionService.currentSelectedNodes; if (selection != null && nodes.length == 1) { + _selectionChangeBySlash = true; TransactionBuilder(widget.editorState) ..deleteText( nodes.first as TextNode, - selection.start.offset - 1, - 1, + selection.start.offset - length, + length, + ) + ..commit(); + } + } + + void _insertText(String text) { + final selection = + widget.editorState.service.selectionService.currentSelection.value; + final nodes = + widget.editorState.service.selectionService.currentSelectedNodes; + if (selection != null && nodes.length == 1) { + _selectionChangeBySlash = true; + TransactionBuilder(widget.editorState) + ..insertText( + nodes.first as TextNode, + selection.end.offset, + text, ) ..commit(); } @@ -318,12 +413,14 @@ class _PopupListItemWidget extends StatelessWidget { class PopupListItem { PopupListItem({ required this.text, + required this.keywords, this.message = '', required this.icon, required this.handler, }); final String text; + final List keywords; final String message; final Widget icon; final void Function(EditorState editorState) handler; From 1667d14e90e38a27d5be0f36bd5fa3c1f63ef940 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:08:34 +0800 Subject: [PATCH 077/224] chore: rename checkbox to to-do list --- .../internal_key_event_handlers/slash_handler.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index fd0df50fb8..8316ea85d5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -47,14 +47,14 @@ final List _popupListItems = [ icon: _popupListIcon('bullets'), handler: (editorState) => insertBulletedListAfterSelection(editorState), ), + // PopupListItem( + // text: 'Numbered list', + // keywords: ['numbered list'], + // icon: _popupListIcon('number'), + // handler: (editorState) => debugPrint('Not implement yet!'), + // ), PopupListItem( - text: 'Numbered list', - keywords: ['numbered list'], - icon: _popupListIcon('number'), - handler: (editorState) => debugPrint('Not implement yet!'), - ), - PopupListItem( - text: 'Checkboxes', + text: 'To-do List', keywords: ['checkbox'], icon: _popupListIcon('checkbox'), handler: (editorState) => insertCheckboxAfterSelection(editorState), From 716d1f93e754a9c5a2e86b46e461e3675999a5aa Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 11 Aug 2022 17:21:02 +0800 Subject: [PATCH 078/224] feat: page up down handler --- .../default_key_event_handlers.dart | 2 ++ .../page_up_down_handler.dart | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index f99caab04a..e617804a77 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -8,6 +8,7 @@ import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handl import 'package:flowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; List defaultKeyEventHandlers = [ @@ -21,4 +22,5 @@ List defaultKeyEventHandlers = [ updateTextStyleByCommandXHandler, whiteSpaceHandler, selectAllHandler, + pageUpDownHandler, ]; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart new file mode 100644 index 0000000000..5acdcdcb0d --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart @@ -0,0 +1,31 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +double? getEditorHeight(EditorState editorState) { + final renderObj = + editorState.service.scrollServiceKey.currentContext?.findRenderObject(); + if (renderObj is RenderBox) { + return renderObj.size.height; + } + return null; +} + +FlowyKeyEventHandler pageUpDownHandler = (editorState, event) { + if (event.logicalKey == LogicalKeyboardKey.pageUp) { + final scrollHeight = getEditorHeight(editorState); + final scrollService = editorState.service.scrollService; + if (scrollHeight != null && scrollService != null) { + scrollService.scrollTo(scrollService.dy - scrollHeight); + } + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.pageDown) { + final scrollHeight = getEditorHeight(editorState); + final scrollService = editorState.service.scrollService; + if (scrollHeight != null && scrollService != null) { + scrollService.scrollTo(scrollService.dy + scrollHeight); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; From 6913550f929eee834ed9c90ffa722cc68d96dd48 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:24:25 +0800 Subject: [PATCH 079/224] fix: the popup list position is too lower --- .../internal_key_event_handlers/slash_handler.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 8316ea85d5..2023454a79 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -82,13 +82,10 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { if (selection == null || context == null || selectable == null) { return KeyEventResult.ignored; } - - final rect = selectable.getCursorRectInPosition(selection.start); - if (rect == null) { + final selectionRects = editorState.service.selectionService.selectionRects; + if (selectionRects.isEmpty) { return KeyEventResult.ignored; } - final offset = selectable.localToGlobal(rect.topLeft); - TransactionBuilder(editorState) ..replaceText(textNode, selection.start.offset, selection.end.offset - selection.start.offset, event.character ?? '') @@ -96,7 +93,8 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { _editorState = editorState; WidgetsBinding.instance.addPostFrameCallback((_) { - showPopupList(context, editorState, offset); + _selectionChangeBySlash = false; + showPopupList(context, editorState, selectionRects.first.bottomRight); }); return KeyEventResult.handled; @@ -107,8 +105,8 @@ void showPopupList( _popupListOverlay?.remove(); _popupListOverlay = OverlayEntry( builder: (context) => Positioned( - top: offset.dy + 20.0, - left: offset.dx + 5.0, + top: offset.dy, + left: offset.dx, child: PopupListWidget( editorState: editorState, items: _popupListItems, From 508b276a79cd2f0a3cf4022abd9f312226bb7f27 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 17:46:20 +0800 Subject: [PATCH 080/224] feat: dismiss popup list if no results --- .../slash_handler.dart | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 2023454a79..83f1b9e13a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -55,7 +55,7 @@ final List _popupListItems = [ // ), PopupListItem( text: 'To-do List', - keywords: ['checkbox'], + keywords: ['checkbox', 'todo'], icon: _popupListIcon('checkbox'), handler: (editorState) => insertCheckboxAfterSelection(editorState), ), @@ -165,16 +165,36 @@ class _PopupListWidgetState extends State { final _focusNode = FocusNode(debugLabel: 'popup_list_widget'); int _selectedIndex = 0; List _items = []; + + int _maxKeywordLength = 0; + String __keyword = ''; String get _keyword => __keyword; set _keyword(String keyword) { __keyword = keyword; - setState(() { - _items = widget.items - .where((item) => - item.keywords.any((keyword) => keyword.contains(_keyword))) - .toList(growable: false); - }); + + final items = widget.items + .where((item) => + item.keywords.any((keyword) => keyword.contains(_keyword))) + .toList(growable: false); + if (items.isNotEmpty) { + var maxKeywordLength = 0; + for (var item in _items) { + for (var keyword in item.keywords) { + maxKeywordLength = max(maxKeywordLength, keyword.length); + } + } + _maxKeywordLength = maxKeywordLength; + } + + if (keyword.length >= _maxKeywordLength + 2) { + clearPopupList(); + } else { + setState(() { + _selectedIndex = 0; + _items = items; + }); + } } @override @@ -213,10 +233,7 @@ class _PopupListWidgetState extends State { borderRadius: BorderRadius.circular(6.0), ), child: _items.isEmpty - ? Align( - alignment: Alignment.centerLeft, - child: _buildNoResultsWidget(context), - ) + ? _buildNoResultsWidget(context) : Row( crossAxisAlignment: CrossAxisAlignment.start, children: _buildColumns(_items, _selectedIndex), @@ -226,11 +243,16 @@ class _PopupListWidgetState extends State { } Widget _buildNoResultsWidget(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'No results', - style: TextStyle(color: Colors.grey, fontSize: 15.0), + return const Align( + alignment: Alignment.centerLeft, + child: Material( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Text( + 'No results', + style: TextStyle(color: Colors.grey), + ), + ), ), ); } @@ -296,15 +318,6 @@ class _PopupListWidgetState extends State { !arrowKeys.contains(event.logicalKey)) { _keyword += event.character!; _insertText(event.character!); - var maxKeywordLength = 0; - for (final item in _items) { - for (final keyword in item.keywords) { - maxKeywordLength = max(keyword.length, maxKeywordLength); - } - } - if (_keyword.length >= maxKeywordLength + 2) { - clearPopupList(); - } return KeyEventResult.handled; } From 96cdb82ca4d3ba0b30e174f4f04b1fe9c51b15e3 Mon Sep 17 00:00:00 2001 From: Chirag Bargoojar Date: Thu, 11 Aug 2022 15:57:55 +0530 Subject: [PATCH 081/224] fix: removed .then() handler --- .../flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index 78aa3d0b7f..5db6afacef 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -21,11 +21,8 @@ extension IntoDialog on Widget { child: this, ), context, - ).then( - (value) { - dialogFocusNode.dispose(); - }, ); + dialogFocusNode.dispose(); } } From f708afe67325199d783bfc6291b2b2ba5ee24563 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 11 Aug 2022 20:14:36 +0800 Subject: [PATCH 082/224] fix: #827 --- .../lib/src/document/selection.dart | 2 + .../src/extensions/text_node_extensions.dart | 9 ++-- .../format_rich_text_style.dart | 47 +++++++++---------- .../lib/src/service/selection_service.dart | 1 + 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart index 68aecba8fc..641a985577 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart @@ -46,6 +46,8 @@ class Selection { (start.path <= end.path && !pathEquals(start.path, end.path)) || (isSingle && start.offset < end.offset); + Selection get reversed => copyWith(start: end, end: start); + Selection collapse({bool atStart = false}) { if (atStart) { return Selection(start: start, end: start); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart index 131255ab63..3408546c42 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart @@ -20,14 +20,17 @@ extension TextNodeExtension on TextNode { bool allSatisfyInSelection(String styleKey, Selection selection) { final ops = delta.whereType(); + final startOffset = + selection.isBackward ? selection.start.offset : selection.end.offset; + final endOffset = + selection.isBackward ? selection.end.offset : selection.start.offset; var start = 0; for (final op in ops) { - if (start >= selection.end.offset) { + if (start >= endOffset) { break; } final length = op.length; - if (start < selection.end.offset && - start + length > selection.start.offset) { + if (start < endOffset && start + length > startOffset) { if (op.attributes == null || !op.attributes!.containsKey(styleKey) || op.attributes![styleKey] == false) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index 6830dd62e4..64cd4f332e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -43,7 +43,7 @@ bool insertTextNodeAfterSelection( } final node = nodes.first; - if (node is TextNode && node.delta.length == 0) { + if (node is TextNode && node.delta.isEmpty) { formatTextNodes(editorState, attributes); } else { final next = selection.end.path.next; @@ -157,11 +157,18 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey) { } bool formatRichTextStyle(EditorState editorState, Attributes attributes) { - final selection = editorState.service.selectionService.currentSelection.value; - final nodes = editorState.service.selectionService.currentSelectedNodes; - final textNodes = nodes.whereType().toList(); + var selection = editorState.service.selectionService.currentSelection.value; + var nodes = editorState.service.selectionService.currentSelectedNodes; - if (selection == null || textNodes.isEmpty) { + if (selection == null) { + return false; + } + + nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); + selection = selection.isBackward ? selection : selection.reversed; + + var textNodes = nodes.whereType().toList(); + if (textNodes.isEmpty) { return false; } @@ -180,28 +187,20 @@ bool formatRichTextStyle(EditorState editorState, Attributes attributes) { } else { for (var i = 0; i < textNodes.length; i++) { final textNode = textNodes[i]; + var index = 0; + var length = textNode.toRawString().length; if (i == 0 && textNode == nodes.first) { - builder.formatText( - textNode, - selection.start.offset, - textNode.toRawString().length - selection.start.offset, - attributes, - ); + index = selection.start.offset; + length = textNode.toRawString().length - selection.start.offset; } else if (i == textNodes.length - 1 && textNode == nodes.last) { - builder.formatText( - textNode, - 0, - selection.end.offset, - attributes, - ); - } else { - builder.formatText( - textNode, - 0, - textNode.toRawString().length, - attributes, - ); + length = selection.end.offset; } + builder.formatText( + textNode, + index, + length, + attributes, + ); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index 552e9eaf69..ecf013d0a4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -487,6 +487,7 @@ class _FlowySelectionState extends State max = mid - 1; } } + min = min.clamp(start, end); final node = sortedNodes[min]; if (node.children.isNotEmpty && node.children.first.rect.top <= offset.dy) { final children = node.children.toList(growable: false); From 456502586bc31a99f512f14eb8e6e657cf2dcde2 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Thu, 11 Aug 2022 19:50:51 +0800 Subject: [PATCH 083/224] fix: undo attributest --- .../lib/src/document/attributes.dart | 16 +++++----- .../flowy_editor/lib/src/document/node.dart | 31 ++++++++----------- .../lib/src/document/state_tree.dart | 9 +++--- .../lib/src/document/text_delta.dart | 4 +-- .../src/operation/transaction_builder.dart | 6 ++-- .../flowy_editor/test/delta_test.dart | 14 ++++++++- .../flowy_editor/test/flowy_editor_test.dart | 5 ++- 7 files changed, 46 insertions(+), 39 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart index 39a5f4e2ff..1a846eec2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/attributes.dart @@ -22,11 +22,15 @@ Attributes invertAttributes(Attributes? attr, Attributes? base) { }); } -Attributes? composeAttributes(Attributes? a, Attributes? b) { +Attributes? composeAttributes(Attributes? a, Attributes? b, + [bool keepNull = false]) { a ??= {}; b ??= {}; - final Attributes attributes = {}; - attributes.addAll(Map.from(b)..removeWhere((_, value) => value == null)); + Attributes attributes = {...b}; + + if (!keepNull) { + attributes = Map.from(attributes)..removeWhere((_, value) => value == null); + } for (final entry in a.entries) { if (!b.containsKey(entry.key)) { @@ -34,9 +38,5 @@ Attributes? composeAttributes(Attributes? a, Attributes? b) { } } - if (attributes.isEmpty) { - return null; - } - - return attributes; + return attributes.isNotEmpty ? attributes : null; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index ae516478ae..24c3035e90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -8,7 +8,7 @@ class Node extends ChangeNotifier with LinkedListEntry { Node? parent; final String type; final LinkedList children; - final Attributes attributes; + Attributes _attributes; GlobalKey? key; // TODO: abstract a selectable node?? @@ -16,22 +16,24 @@ class Node extends ChangeNotifier with LinkedListEntry { String? get subtype { // TODO: make 'subtype' as a const value. - if (attributes.containsKey('subtype')) { - assert(attributes['subtype'] is String?, + if (_attributes.containsKey('subtype')) { + assert(_attributes['subtype'] is String?, 'subtype must be a [String] or [null]'); - return attributes['subtype'] as String?; + return _attributes['subtype'] as String?; } return null; } Path get path => _path(); + Attributes get attributes => _attributes; + Node({ required this.type, required this.children, - required this.attributes, + required Attributes attributes, this.parent, - }) { + }) : _attributes = attributes { for (final child in children) { child.parent = this; } @@ -84,16 +86,9 @@ class Node extends ChangeNotifier with LinkedListEntry { } void updateAttributes(Attributes attributes) { - bool shouldNotifyParent = - this.attributes['subtype'] != attributes['subtype']; + bool shouldNotifyParent = _attributes['subtype'] != attributes['subtype']; - for (final attribute in attributes.entries) { - if (attribute.value == null) { - this.attributes.remove(attribute.key); - } else { - this.attributes[attribute.key] = attribute.value; - } - } + _attributes = composeAttributes(_attributes, attributes) ?? {}; // Notify the new attributes // if attributes contains 'subtype', should notify parent to rebuild node // else, just notify current node. @@ -149,8 +144,8 @@ class Node extends ChangeNotifier with LinkedListEntry { if (children.isNotEmpty) { map['children'] = children.map((node) => node.toJson()); } - if (attributes.isNotEmpty) { - map['attributes'] = attributes; + if (_attributes.isNotEmpty) { + map['attributes'] = _attributes; } return map; } @@ -214,7 +209,7 @@ class TextNode extends Node { TextNode( type: type ?? this.type, children: children ?? this.children, - attributes: attributes ?? this.attributes, + attributes: attributes ?? _attributes, delta: delta ?? this.delta, ); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart index 199736234a..b356880310 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart @@ -65,16 +65,15 @@ class StateTree { } } - Attributes? update(Path path, Attributes attributes) { + bool update(Path path, Attributes attributes) { if (path.isEmpty) { - return null; + return false; } final updatedNode = root.childAtPath(path); if (updatedNode == null) { - return null; + return false; } - final previousAttributes = Attributes.from(updatedNode.attributes); updatedNode.updateAttributes(attributes); - return previousAttributes; + return true; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 6cbc015569..6a4e4f3c80 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -383,8 +383,8 @@ class Delta extends Iterable { final length = min(thisIter.peekLength(), otherIter.peekLength()); final thisOp = thisIter.next(length); final otherOp = otherIter.next(length); - final attributes = - composeAttributes(thisOp.attributes, otherOp.attributes); + final attributes = composeAttributes( + thisOp.attributes, otherOp.attributes, thisOp is TextRetain); if (otherOp is TextRetain && otherOp.length > 0) { TextOperation? newOp; if (thisOp is TextRetain) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index 305791b519..5a8dc68b3b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -40,10 +40,12 @@ class TransactionBuilder { updateNode(Node node, Attributes attributes) { beforeSelection = state.cursorSelection; + + final inverted = invertAttributes(attributes, node.attributes); add(UpdateOperation( node.path, - Attributes.from(node.attributes)..addAll(attributes), - node.attributes, + {...attributes}, + inverted, )); } diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart index 48182912a3..a835471b55 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/src/document/attributes.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flowy_editor/src/document/text_delta.dart'; @@ -240,7 +241,8 @@ void main() { ..retain(3, {'bold': null}); final inverted = delta.invert(base); expect(expected, inverted); - expect(base.compose(delta).compose(inverted), base); + final t = base.compose(delta).compose(inverted); + expect(t, base); }); }); group('json', () { @@ -314,4 +316,14 @@ void main() { expect(delta.prevRunePosition(0), -1); }); }); + group("attributes", () { + test("compose", () { + final attrs = composeAttributes({"a": null}, {"b": null}, true); + expect(attrs != null, true); + expect(attrs!.containsKey("a"), true); + expect(attrs.containsKey("b"), true); + expect(attrs["a"], null); + expect(attrs["b"], null); + }); + }); } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index e1b97a591d..e070b7f820 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -68,9 +68,8 @@ void main() { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final attributes = stateTree.update([1, 1], {'text-type': 'heading1'}); - expect(attributes != null, true); - expect(attributes!['text-type'], 'checkbox'); + final test = stateTree.update([1, 1], {'text-type': 'heading1'}); + expect(test, true); final updatedNode = stateTree.nodeAtPath([1, 1]); expect(updatedNode != null, true); expect(updatedNode!.attributes['text-type'], 'heading1'); From 461751cf596a98a7a0a46c8501466bbaee67281f Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 11 Aug 2022 21:18:27 +0800 Subject: [PATCH 084/224] chore: config group service --- .../plugins/board/application/board_bloc.dart | 58 ++------- .../flowy-folder/tests/workspace/script.rs | 8 +- .../flowy-grid/src/entities/field_entities.rs | 28 ++-- .../filter_entities/checkbox_filter.rs | 10 +- .../entities/filter_entities/date_filter.rs | 10 +- .../entities/filter_entities/number_filter.rs | 10 +- .../filter_entities/select_option_filter.rs | 10 +- .../entities/filter_entities/text_filter.rs | 10 +- .../src/entities/filter_entities/util.rs | 10 +- .../entities/group_entities/checkbox_group.rs | 7 + .../src/entities/group_entities/date_group.rs | 26 ++++ .../src/entities/group_entities/group.rs | 31 ++--- .../src/entities/group_entities/mod.rs | 12 ++ .../entities/group_entities/number_group.rs | 7 + .../group_entities/select_option_group.rs | 7 + .../src/entities/group_entities/text_group.rs | 7 + .../src/entities/group_entities/url_group.rs | 7 + .../flowy-grid/src/entities/sort_entities.rs | 10 +- .../rust-lib/flowy-grid/src/event_handler.rs | 4 +- frontend/rust-lib/flowy-grid/src/event_map.rs | 2 +- .../src/services/cell/cell_operation.rs | 20 +-- .../src/services/field/field_builder.rs | 8 +- .../date_type_option/date_tests.rs | 16 +-- .../date_type_option/date_type_option.rs | 14 +- .../number_type_option/number_tests.rs | 12 +- .../number_type_option/number_type_option.rs | 16 +-- .../text_type_option/text_type_option.rs | 14 +- .../type_options/url_type_option/url_tests.rs | 18 +-- .../url_type_option/url_type_option.rs | 12 +- .../src/services/filter/filter_cache.rs | 33 +++-- .../src/services/filter/filter_service.rs | 12 +- .../services/filter/impls/checkbox_filter.rs | 14 +- .../src/services/filter/impls/date_filter.rs | 22 ++-- .../services/filter/impls/number_filter.rs | 18 +-- .../filter/impls/select_option_filter.rs | 24 ++-- .../src/services/filter/impls/text_filter.rs | 22 ++-- .../src/services/filter/impls/url_filter.rs | 8 +- .../flowy-grid/src/services/grid_editor.rs | 7 +- .../src/services/group/group_service.rs | 120 ++++++++++++++++++ .../services/group/impls/checkbox_group.rs | 17 +++ .../src/services/group/impls/date_group.rs | 0 .../src/services/group/impls/mod.rs | 11 ++ .../src/services/group/impls/number_group.rs | 0 .../group/impls/select_option_group.rs | 8 ++ .../src/services/group/impls/text_group.rs | 0 .../src/services/group/impls/url_group.rs | 0 .../flowy-grid/src/services/group/mod.rs | 2 + .../tests/grid/block_test/script.rs | 2 +- .../flowy-grid/tests/grid/field_test/util.rs | 2 +- .../src/revision/filter_rev.rs | 2 +- .../src/revision/grid_group.rs | 6 +- .../src/revision/grid_rev.rs | 1 + .../src/revision/grid_setting_rev.rs | 59 +++++---- .../flowy-grid-data-model/src/revision/mod.rs | 2 - .../flowy-grid-data-model/tests/serde_test.rs | 2 +- .../src/client_grid/grid_revision_pad.rs | 27 ++-- shared-lib/flowy-sync/src/entities/grid.rs | 2 +- 57 files changed, 533 insertions(+), 294 deletions(-) create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/date_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs rename shared-lib/flowy-grid-data-model/src/revision/grid_sort.rs => frontend/rust-lib/flowy-grid/src/services/group/impls/date_group.rs (100%) create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/number_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/text_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/url_group.rs diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 581387e5ac..cbfd0500fb 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; -import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; @@ -57,8 +56,8 @@ class BoardBloc extends Bloc { didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - groupByField: (FieldPB field) { - emit(state.copyWith(groupField: Some(field))); + didReceiveGroups: (List groups) { + emit(state.copyWith(groups: groups)); }, ); }, @@ -83,50 +82,20 @@ class BoardBloc extends Bloc { add(BoardEvent.didReceiveGridUpdate(grid)); } }, - onFieldsChanged: (fields) { - if (!isClosed) { - _buildColumns(fields); - } - }, - onGroupChanged: (groups) {}, - onError: (err) { - Log.error(err); - }, - ); - } - - void _buildColumns(UnmodifiableListView fields) { - FieldPB? groupField; - for (final field in fields) { - if (field.fieldType == FieldType.SingleSelect) { - groupField = field; - _buildColumnsFromSingleSelect(field); - } - } - - assert(groupField != null); - add(BoardEvent.groupByField(groupField!)); - } - - void _buildColumnsFromSingleSelect(FieldPB field) { - final typeOptionContext = makeTypeOptionContext( - gridId: _dataController.gridId, - field: field, - ); - - typeOptionContext.loadTypeOptionData( - onCompleted: (singleSelect) { - List columns = singleSelect.options.map((option) { + onGroupChanged: (groups) { + List columns = groups.map((group) { return AFBoardColumnData( - id: option.id, - desc: option.name, - customData: option, + id: group.groupId, + desc: group.desc, + customData: group, ); }).toList(); boardDataController.addColumns(columns); }, - onError: (err) => Log.error(err), + onError: (err) { + Log.error(err); + }, ); } @@ -147,7 +116,8 @@ class BoardBloc extends Bloc { class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = InitialGrid; const factory BoardEvent.createRow() = _CreateRow; - const factory BoardEvent.groupByField(FieldPB field) = _GroupByField; + const factory BoardEvent.didReceiveGroups(List groups) = + _DidReceiveGroup; const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; @@ -158,14 +128,14 @@ class BoardState with _$BoardState { const factory BoardState({ required String gridId, required Option grid, - required Option groupField, + required List groups, required List rowInfos, required GridLoadingState loadingState, }) = _BoardState; factory BoardState.initial(String gridId) => BoardState( rowInfos: [], - groupField: none(), + groups: [], grid: none(), gridId: gridId, loadingState: const _Loading(), diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index 3292d6deea..5fb0be874c 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -5,6 +5,7 @@ use flowy_folder::entities::{ trash::{RepeatedTrashPB, TrashIdPB, TrashType}, view::{CreateViewPayloadPB, UpdateViewPayloadPB}, workspace::{CreateWorkspacePayloadPB, RepeatedWorkspacePB}, + SubViewDataTypePB, }; use flowy_folder::entities::{ app::{AppPB, RepeatedAppPB}, @@ -353,13 +354,18 @@ pub async fn create_view( desc: &str, data_type: ViewDataTypePB, ) -> ViewPB { + let sub_data_type = match data_type { + ViewDataTypePB::TextBlock => None, + ViewDataTypePB::Database => Some(SubViewDataTypePB::Grid), + }; + let request = CreateViewPayloadPB { belong_to_id: app_id.to_string(), name: name.to_string(), desc: desc.to_string(), thumbnail: None, data_type, - sub_data_type: None, + sub_data_type, plugin_type: 0, data: vec![], }; diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index 468b216b5b..3dc296f34e 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -518,6 +518,14 @@ pub enum FieldType { URL = 6, } +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; + impl std::default::Default for FieldType { fn default() -> Self { FieldType::RichText @@ -549,35 +557,39 @@ impl FieldType { } pub fn is_number(&self) -> bool { - self == &FieldType::Number + self == &NUMBER_FIELD } pub fn is_text(&self) -> bool { - self == &FieldType::RichText + self == &RICH_TEXT_FIELD } pub fn is_checkbox(&self) -> bool { - self == &FieldType::Checkbox + self == &CHECKBOX_FIELD } pub fn is_date(&self) -> bool { - self == &FieldType::DateTime + self == &DATE_FIELD } pub fn is_single_select(&self) -> bool { - self == &FieldType::SingleSelect + self == &SINGLE_SELECT_FIELD } pub fn is_multi_select(&self) -> bool { - self == &FieldType::MultiSelect + self == &MULTI_SELECT_FIELD } pub fn is_url(&self) -> bool { - self == &FieldType::URL + self == &URL_FIELD } pub fn is_select_option(&self) -> bool { - self == &FieldType::MultiSelect || self == &FieldType::SingleSelect + self == &MULTI_SELECT_FIELD || self == &SINGLE_SELECT_FIELD + } + + pub fn can_be_group(&self) -> bool { + self.is_select_option() } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs index bb31b7a3be..45a64af245 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs @@ -1,10 +1,10 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use flowy_grid_data_model::revision::GridFilterRevision; +use flowy_grid_data_model::revision::FilterConfigurationRevision; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridCheckboxFilter { +pub struct CheckboxFilterConfigurationPB { #[pb(index = 1)] pub condition: CheckboxCondition, } @@ -40,9 +40,9 @@ impl std::convert::TryFrom for CheckboxCondition { } } -impl std::convert::From> for GridCheckboxFilter { - fn from(rev: Arc) -> Self { - GridCheckboxFilter { +impl std::convert::From> for CheckboxFilterConfigurationPB { + fn from(rev: Arc) -> Self { + CheckboxFilterConfigurationPB { condition: CheckboxCondition::try_from(rev.condition).unwrap_or(CheckboxCondition::IsChecked), } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/date_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/date_filter.rs index 936b95216c..72be45a655 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/date_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/date_filter.rs @@ -2,13 +2,13 @@ use crate::entities::FieldType; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::GridFilterRevision; +use flowy_grid_data_model::revision::FilterConfigurationRevision; use serde::{Deserialize, Serialize}; use std::str::FromStr; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridDateFilter { +pub struct DateFilterConfigurationPB { #[pb(index = 1)] pub condition: DateFilterCondition, @@ -120,10 +120,10 @@ impl std::convert::TryFrom for DateFilterCondition { } } } -impl std::convert::From> for GridDateFilter { - fn from(rev: Arc) -> Self { +impl std::convert::From> for DateFilterConfigurationPB { + fn from(rev: Arc) -> Self { let condition = DateFilterCondition::try_from(rev.condition).unwrap_or(DateFilterCondition::DateIs); - let mut filter = GridDateFilter { + let mut filter = DateFilterConfigurationPB { condition, ..Default::default() }; diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs index 097ff5330a..68c7474b8f 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs @@ -1,11 +1,11 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use flowy_grid_data_model::revision::GridFilterRevision; +use flowy_grid_data_model::revision::FilterConfigurationRevision; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridNumberFilter { +pub struct NumberFilterConfigurationPB { #[pb(index = 1)] pub condition: NumberFilterCondition, @@ -55,9 +55,9 @@ impl std::convert::TryFrom for NumberFilterCondition { } } -impl std::convert::From> for GridNumberFilter { - fn from(rev: Arc) -> Self { - GridNumberFilter { +impl std::convert::From> for NumberFilterConfigurationPB { + fn from(rev: Arc) -> Self { + NumberFilterConfigurationPB { condition: NumberFilterCondition::try_from(rev.condition).unwrap_or(NumberFilterCondition::Equal), content: rev.content.clone(), } diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs index 9eb4ff3fe9..47e07c0b73 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs @@ -1,11 +1,11 @@ use crate::services::field::SelectOptionIds; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use flowy_grid_data_model::revision::GridFilterRevision; +use flowy_grid_data_model::revision::FilterConfigurationRevision; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridSelectOptionFilter { +pub struct SelectOptionFilterConfigurationPB { #[pb(index = 1)] pub condition: SelectOptionCondition, @@ -47,10 +47,10 @@ impl std::convert::TryFrom for SelectOptionCondition { } } -impl std::convert::From> for GridSelectOptionFilter { - fn from(rev: Arc) -> Self { +impl std::convert::From> for SelectOptionFilterConfigurationPB { + fn from(rev: Arc) -> Self { let ids = SelectOptionIds::from(rev.content.clone()); - GridSelectOptionFilter { + SelectOptionFilterConfigurationPB { condition: SelectOptionCondition::try_from(rev.condition).unwrap_or(SelectOptionCondition::OptionIs), option_ids: ids.into_inner(), } diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs index 7335e89129..802941516d 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs @@ -1,10 +1,10 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use flowy_grid_data_model::revision::GridFilterRevision; +use flowy_grid_data_model::revision::FilterConfigurationRevision; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridTextFilter { +pub struct TextFilterConfigurationPB { #[pb(index = 1)] pub condition: TextFilterCondition, @@ -54,9 +54,9 @@ impl std::convert::TryFrom for TextFilterCondition { } } -impl std::convert::From> for GridTextFilter { - fn from(rev: Arc) -> Self { - GridTextFilter { +impl std::convert::From> for TextFilterConfigurationPB { + fn from(rev: Arc) -> Self { + TextFilterConfigurationPB { condition: TextFilterCondition::try_from(rev.condition).unwrap_or(TextFilterCondition::Is), content: rev.content.clone(), } diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs index b085f6bdd3..687b371345 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs @@ -5,7 +5,7 @@ use crate::entities::{ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::{FieldRevision, GridFilterRevision}; +use flowy_grid_data_model::revision::{FieldRevision, FilterConfigurationRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams}; use std::convert::TryInto; use std::sync::Arc; @@ -22,14 +22,14 @@ pub struct RepeatedGridConfigurationFilterPB { pub items: Vec, } -impl std::convert::From<&GridFilterRevision> for GridFilterConfiguration { - fn from(rev: &GridFilterRevision) -> Self { +impl std::convert::From<&FilterConfigurationRevision> for GridFilterConfiguration { + fn from(rev: &FilterConfigurationRevision) -> Self { Self { id: rev.id.clone() } } } -impl std::convert::From>> for RepeatedGridConfigurationFilterPB { - fn from(revs: Vec>) -> Self { +impl std::convert::From>> for RepeatedGridConfigurationFilterPB { + fn from(revs: Vec>) -> Self { RepeatedGridConfigurationFilterPB { items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs new file mode 100644 index 0000000000..ba121694a7 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs @@ -0,0 +1,7 @@ +use flowy_derive::ProtoBuf; + +#[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-grid/src/entities/group_entities/date_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/date_group.rs new file mode 100644 index 0000000000..c117b22f8b --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/date_group.rs @@ -0,0 +1,26 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[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 + } +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index c27b4aaa93..4a0f56e425 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -2,7 +2,7 @@ use crate::entities::{FieldType, RowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::GridGroupRevision; +use flowy_grid_data_model::revision::GroupConfigurationRevision; use flowy_sync::entities::grid::{CreateGridGroupParams, DeleteGroupParams}; use std::convert::TryInto; use std::sync::Arc; @@ -14,17 +14,13 @@ pub struct GridGroupConfigurationPB { #[pb(index = 2)] pub group_field_id: String, - - #[pb(index = 3, one_of)] - pub sub_group_field_id: Option, } -impl std::convert::From<&GridGroupRevision> for GridGroupConfigurationPB { - fn from(rev: &GridGroupRevision) -> Self { +impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationPB { + fn from(rev: &GroupConfigurationRevision) -> Self { GridGroupConfigurationPB { id: rev.id.clone(), group_field_id: rev.field_id.clone(), - sub_group_field_id: rev.sub_field_id.clone(), } } } @@ -32,7 +28,7 @@ impl std::convert::From<&GridGroupRevision> for GridGroupConfigurationPB { #[derive(ProtoBuf, Debug, Default, Clone)] pub struct RepeatedGridGroupPB { #[pb(index = 1)] - items: Vec, + pub(crate) items: Vec, } #[derive(ProtoBuf, Debug, Default, Clone)] @@ -59,8 +55,8 @@ impl std::convert::From> for RepeatedGridGroupConf } } -impl std::convert::From>> for RepeatedGridGroupConfigurationPB { - fn from(revs: Vec>) -> Self { +impl std::convert::From>> for RepeatedGridGroupConfigurationPB { + fn from(revs: Vec>) -> Self { RepeatedGridGroupConfigurationPB { items: revs.iter().map(|rev| rev.as_ref().into()).collect(), } @@ -72,11 +68,11 @@ pub struct CreateGridGroupPayloadPB { #[pb(index = 1)] pub field_id: String, - #[pb(index = 2, one_of)] - pub sub_field_id: Option, - - #[pb(index = 3)] + #[pb(index = 2)] pub field_type: FieldType, + + #[pb(index = 3, one_of)] + pub content: Option>, } impl TryInto for CreateGridGroupPayloadPB { @@ -87,15 +83,10 @@ impl TryInto for CreateGridGroupPayloadPB { .map_err(|_| ErrorCode::FieldIdIsEmpty)? .0; - let sub_field_id = match self.sub_field_id { - None => None, - Some(field_id) => Some(NotEmptyStr::parse(field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?.0), - }; - Ok(CreateGridGroupParams { field_id, - sub_field_id, field_type_rev: self.field_type.into(), + content: self.content, }) } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs index b50e59f6d6..1ededd3188 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs @@ -1,3 +1,15 @@ +mod checkbox_group; +mod date_group; mod group; +mod number_group; +mod select_option_group; +mod text_group; +mod url_group; +pub use checkbox_group::*; +pub use date_group::*; pub use group::*; +pub use number_group::*; +pub use select_option_group::*; +pub use text_group::*; +pub use url_group::*; diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs new file mode 100644 index 0000000000..ddb63a6ce5 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs @@ -0,0 +1,7 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct NumberGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs new file mode 100644 index 0000000000..67dfe8d1b7 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs @@ -0,0 +1,7 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct SelectOptionGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs new file mode 100644 index 0000000000..b349850843 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs @@ -0,0 +1,7 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct TextGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs new file mode 100644 index 0000000000..955cb60ae3 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs @@ -0,0 +1,7 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct UrlGroupConfigurationPB { + #[pb(index = 1)] + hide_empty: bool, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs index b630b000c5..f844b75066 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs @@ -1,7 +1,7 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::GridSortRevision; +use flowy_grid_data_model::revision::SortConfigurationRevision; use flowy_sync::entities::grid::CreateGridSortParams; use std::convert::TryInto; use std::sync::Arc; @@ -15,8 +15,8 @@ pub struct GridSort { pub field_id: Option, } -impl std::convert::From<&GridSortRevision> for GridSort { - fn from(rev: &GridSortRevision) -> Self { +impl std::convert::From<&SortConfigurationRevision> for GridSort { + fn from(rev: &SortConfigurationRevision) -> Self { GridSort { id: rev.id.clone(), @@ -31,8 +31,8 @@ pub struct RepeatedGridSortPB { pub items: Vec, } -impl std::convert::From>> for RepeatedGridSortPB { - fn from(revs: Vec>) -> Self { +impl std::convert::From>> for RepeatedGridSortPB { + fn from(revs: Vec>) -> Self { RepeatedGridSortPB { items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), } diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 4721e70bca..86b8e8892c 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -407,12 +407,12 @@ pub(crate) async fn update_date_cell_handler( } #[tracing::instrument(level = "trace", skip_all, err)] -pub(crate) async fn get_group_handler( +pub(crate) async fn get_groups_handler( data: Data, manager: AppData>, ) -> DataResult { let params: GridIdPB = data.into_inner(); let editor = manager.get_grid_editor(¶ms.value)?; - let group = editor.get_group().await?; + let group = editor.load_groups().await?; data_result(group) } diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 0852deade2..9e0b2ef5dd 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -39,7 +39,7 @@ pub fn create(grid_manager: Arc) -> Module { // Date .event(GridEvent::UpdateDateCell, update_date_cell_handler) // Group - .event(GridEvent::GetGroup, update_date_cell_handler); + .event(GridEvent::GetGroup, get_groups_handler); module } diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index e73dbb8bc3..7ebee15d53 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -11,6 +11,10 @@ pub trait CellFilterOperation { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult; } +pub trait CellGroupOperation { + fn apply_group(&self, any_cell_data: AnyCellData, group_content: &str) -> FlowyResult; +} + /// Return object that describes the cell. pub trait CellDisplayable { fn display_data( @@ -55,15 +59,15 @@ pub fn apply_cell_data_changeset>( let changeset = changeset.to_string(); let field_type = field_rev.field_type_rev.into(); let s = match field_type { - FieldType::RichText => RichTextTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), - FieldType::Number => NumberTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), - FieldType::DateTime => DateTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::RichText => RichTextTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::Number => NumberTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::DateTime => DateTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::SingleSelect => { SingleSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev) } FieldType::MultiSelect => MultiSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::Checkbox => CheckboxTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), - FieldType::URL => URLTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::URL => URLTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), }?; Ok(AnyCellData::new(s, field_type).json()) @@ -97,13 +101,13 @@ pub fn try_decode_cell_data( let field_type: FieldTypeRevision = t_field_type.into(); let data = match t_field_type { FieldType::RichText => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::Number => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::DateTime => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::SingleSelect => field_rev .get_type_option_entry::(field_type)? @@ -115,7 +119,7 @@ pub fn try_decode_cell_data( .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::URL => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), }; Some(data) diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs index ccd16a019e..52f23d4906 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs @@ -90,13 +90,13 @@ pub trait TypeOptionBuilder { pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box { let s: String = match field_type { - FieldType::RichText => RichTextTypeOption::default().into(), - FieldType::Number => NumberTypeOption::default().into(), - FieldType::DateTime => DateTypeOption::default().into(), + FieldType::RichText => RichTextTypeOptionPB::default().into(), + FieldType::Number => NumberTypeOptionPB::default().into(), + FieldType::DateTime => DateTypeOptionPB::default().into(), FieldType::SingleSelect => SingleSelectTypeOptionPB::default().into(), FieldType::MultiSelect => MultiSelectTypeOptionPB::default().into(), FieldType::Checkbox => CheckboxTypeOption::default().into(), - FieldType::URL => URLTypeOption::default().into(), + FieldType::URL => URLTypeOptionPB::default().into(), }; type_option_builder_from_json_str(&s, field_type) diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs index 1229a8f5e1..5bdc39ed71 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs @@ -9,7 +9,7 @@ mod tests { #[test] fn date_type_option_date_format_test() { - let mut type_option = DateTypeOption::default(); + let mut type_option = DateTypeOptionPB::default(); let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); for date_format in DateFormat::iter() { type_option.date_format = date_format; @@ -32,7 +32,7 @@ mod tests { #[test] fn date_type_option_different_time_format_test() { - let mut type_option = DateTypeOption::default(); + let mut type_option = DateTypeOptionPB::default(); let field_type = FieldType::DateTime; let field_rev = FieldBuilder::from_field_type(&field_type).build(); @@ -66,7 +66,7 @@ mod tests { #[test] fn date_type_option_invalid_date_str_test() { - let type_option = DateTypeOption::default(); + let type_option = DateTypeOptionPB::default(); let field_type = FieldType::DateTime; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_date(&type_option, "abc", None, "", &field_rev); @@ -75,7 +75,7 @@ mod tests { #[test] #[should_panic] fn date_type_option_invalid_include_time_str_test() { - let mut type_option = DateTypeOption::new(); + let mut type_option = DateTypeOptionPB::new(); type_option.include_time = true; let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); @@ -90,7 +90,7 @@ mod tests { #[test] fn date_type_option_empty_include_time_str_test() { - let mut type_option = DateTypeOption::new(); + let mut type_option = DateTypeOptionPB::new(); type_option.include_time = true; let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); @@ -101,7 +101,7 @@ mod tests { #[test] #[should_panic] fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { - let mut type_option = DateTypeOption::new(); + let mut type_option = DateTypeOptionPB::new(); type_option.include_time = true; let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); @@ -114,7 +114,7 @@ mod tests { ); } fn assert_date( - type_option: &DateTypeOption, + type_option: &DateTypeOptionPB, timestamp: T, include_time_str: Option, expected_str: &str, @@ -133,7 +133,7 @@ mod tests { ); } - fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String { + fn decode_cell_data(encoded_data: String, type_option: &DateTypeOptionPB, field_rev: &FieldRevision) -> String { let decoded_data = type_option .decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev) .unwrap() diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs index 17b897b1f0..729ae1958a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; // Date #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct DateTypeOption { +pub struct DateTypeOptionPB { #[pb(index = 1)] pub date_format: DateFormat, @@ -24,9 +24,9 @@ pub struct DateTypeOption { #[pb(index = 3)] pub include_time: bool, } -impl_type_option!(DateTypeOption, FieldType::DateTime); +impl_type_option!(DateTypeOptionPB, FieldType::DateTime); -impl DateTypeOption { +impl DateTypeOptionPB { #[allow(dead_code)] pub fn new() -> Self { Self::default() @@ -116,7 +116,7 @@ impl DateTypeOption { } } -impl CellDisplayable for DateTypeOption { +impl CellDisplayable for DateTypeOptionPB { fn display_data( &self, cell_data: CellData, @@ -129,7 +129,7 @@ impl CellDisplayable for DateTypeOption { } } -impl CellDataOperation for DateTypeOption { +impl CellDataOperation for DateTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, @@ -169,9 +169,9 @@ impl CellDataOperation for DateTypeOption { } #[derive(Default)] -pub struct DateTypeOptionBuilder(DateTypeOption); +pub struct DateTypeOptionBuilder(DateTypeOptionPB); impl_into_box_type_option_builder!(DateTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption); +impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOptionPB); impl DateTypeOptionBuilder { pub fn date_format(mut self, date_format: DateFormat) -> Self { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs index 41132ecfe4..41d4cb212c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs @@ -3,14 +3,14 @@ mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; use crate::services::field::FieldBuilder; - use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption}; + use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOptionPB}; use flowy_grid_data_model::revision::FieldRevision; use strum::IntoEnumIterator; /// Testing when the input is not a number. #[test] fn number_type_option_invalid_input_test() { - let type_option = NumberTypeOption::default(); + let type_option = NumberTypeOptionPB::default(); let field_type = FieldType::Number; let field_rev = FieldBuilder::from_field_type(&field_type).build(); @@ -33,7 +33,7 @@ mod tests { /// Format the input number to the corresponding format string. #[test] fn number_type_option_format_number_test() { - let mut type_option = NumberTypeOption::default(); + let mut type_option = NumberTypeOptionPB::default(); let field_type = FieldType::Number; let field_rev = FieldBuilder::from_field_type(&field_type).build(); @@ -63,7 +63,7 @@ mod tests { /// Format the input String to the corresponding format string. #[test] fn number_type_option_format_str_test() { - let mut type_option = NumberTypeOption::default(); + let mut type_option = NumberTypeOptionPB::default(); let field_type = FieldType::Number; let field_rev = FieldBuilder::from_field_type(&field_type).build(); @@ -101,7 +101,7 @@ mod tests { /// Carry out the sign positive to input number #[test] fn number_description_sign_test() { - let mut type_option = NumberTypeOption { + let mut type_option = NumberTypeOptionPB { sign_positive: false, ..Default::default() }; @@ -129,7 +129,7 @@ mod tests { } fn assert_number( - type_option: &NumberTypeOption, + type_option: &NumberTypeOptionPB, input_str: &str, expected_str: &str, field_type: &FieldType, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs index 26d1d64248..cdb1118385 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs @@ -14,9 +14,9 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; #[derive(Default)] -pub struct NumberTypeOptionBuilder(NumberTypeOption); +pub struct NumberTypeOptionBuilder(NumberTypeOptionPB); impl_into_box_type_option_builder!(NumberTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption); +impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOptionPB); impl NumberTypeOptionBuilder { pub fn name(mut self, name: &str) -> Self { @@ -52,7 +52,7 @@ impl TypeOptionBuilder for NumberTypeOptionBuilder { // Number #[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)] -pub struct NumberTypeOption { +pub struct NumberTypeOptionPB { #[pb(index = 1)] pub format: NumberFormat, @@ -68,9 +68,9 @@ pub struct NumberTypeOption { #[pb(index = 5)] pub name: String, } -impl_type_option!(NumberTypeOption, FieldType::Number); +impl_type_option!(NumberTypeOptionPB, FieldType::Number); -impl NumberTypeOption { +impl NumberTypeOptionPB { pub fn new() -> Self { Self::default() } @@ -102,7 +102,7 @@ pub(crate) fn strip_currency_symbol(s: T) -> String { s } -impl CellDataOperation for NumberTypeOption { +impl CellDataOperation for NumberTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, @@ -132,11 +132,11 @@ impl CellDataOperation for NumberTypeOption { } } -impl std::default::Default for NumberTypeOption { +impl std::default::Default for NumberTypeOptionPB { fn default() -> Self { let format = NumberFormat::default(); let symbol = format.symbol(); - NumberTypeOption { + NumberTypeOptionPB { format, scale: 0, symbol, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index 44ae81315d..00ea95125f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -12,9 +12,9 @@ use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDat use serde::{Deserialize, Serialize}; #[derive(Default)] -pub struct RichTextTypeOptionBuilder(RichTextTypeOption); +pub struct RichTextTypeOptionBuilder(RichTextTypeOptionPB); impl_into_box_type_option_builder!(RichTextTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(RichTextTypeOptionBuilder, RichTextTypeOption); +impl_builder_from_json_str_and_from_bytes!(RichTextTypeOptionBuilder, RichTextTypeOptionPB); impl TypeOptionBuilder for RichTextTypeOptionBuilder { fn field_type(&self) -> FieldType { @@ -27,13 +27,13 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder { } #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)] -pub struct RichTextTypeOption { +pub struct RichTextTypeOptionPB { #[pb(index = 1)] data: String, //It's not used yet } -impl_type_option!(RichTextTypeOption, FieldType::RichText); +impl_type_option!(RichTextTypeOptionPB, FieldType::RichText); -impl CellDisplayable for RichTextTypeOption { +impl CellDisplayable for RichTextTypeOptionPB { fn display_data( &self, cell_data: CellData, @@ -45,7 +45,7 @@ impl CellDisplayable for RichTextTypeOption { } } -impl CellDataOperation for RichTextTypeOption { +impl CellDataOperation for RichTextTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, @@ -114,7 +114,7 @@ mod tests { #[test] fn text_description_test() { - let type_option = RichTextTypeOption::default(); + let type_option = RichTextTypeOptionPB::default(); // date let field_type = FieldType::DateTime; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs index 3cf5d6f99b..ddccfdb606 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs @@ -3,14 +3,14 @@ mod tests { use crate::entities::FieldType; use crate::services::cell::{CellData, CellDataOperation}; use crate::services::field::{FieldBuilder, URLCellDataParser}; - use crate::services::field::{URLCellDataPB, URLTypeOption}; + use crate::services::field::{URLCellDataPB, URLTypeOptionPB}; use flowy_grid_data_model::revision::FieldRevision; /// 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 type_option = URLTypeOptionPB::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_url(&type_option, "123", "123", "", &field_type, &field_rev); @@ -21,7 +21,7 @@ mod tests { /// if there's a http url in the input string. #[test] fn url_type_option_contains_url_test() { - let type_option = URLTypeOption::default(); + let type_option = URLTypeOptionPB::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_url( @@ -46,7 +46,7 @@ mod tests { /// 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 type_option = URLTypeOptionPB::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_url( @@ -71,7 +71,7 @@ mod tests { /// 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 type_option = URLTypeOptionPB::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_url( @@ -96,7 +96,7 @@ mod tests { /// if there's a level4 url in the input string. #[test] fn level4_url_type_test() { - let type_option = URLTypeOption::default(); + let type_option = URLTypeOptionPB::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_url( @@ -121,7 +121,7 @@ mod tests { /// urls with different top level domains. #[test] fn different_top_level_domains_test() { - let type_option = URLTypeOption::default(); + let type_option = URLTypeOptionPB::default(); let field_type = FieldType::URL; let field_rev = FieldBuilder::from_field_type(&field_type).build(); assert_url( @@ -162,7 +162,7 @@ mod tests { } fn assert_url( - type_option: &URLTypeOption, + type_option: &URLTypeOptionPB, input_str: &str, expected_str: &str, expected_url: &str, @@ -177,7 +177,7 @@ mod tests { fn decode_cell_data>>( encoded_data: T, - type_option: &URLTypeOption, + type_option: &URLTypeOptionPB, field_rev: &FieldRevision, field_type: &FieldType, ) -> URLCellDataPB { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs index 3fa49dac09..adbc91b4f0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs @@ -11,9 +11,9 @@ use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; #[derive(Default)] -pub struct URLTypeOptionBuilder(URLTypeOption); +pub struct URLTypeOptionBuilder(URLTypeOptionPB); impl_into_box_type_option_builder!(URLTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption); +impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOptionPB); impl TypeOptionBuilder for URLTypeOptionBuilder { fn field_type(&self) -> FieldType { @@ -26,13 +26,13 @@ impl TypeOptionBuilder for URLTypeOptionBuilder { } #[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] -pub struct URLTypeOption { +pub struct URLTypeOptionPB { #[pb(index = 1)] data: String, //It's not used yet. } -impl_type_option!(URLTypeOption, FieldType::URL); +impl_type_option!(URLTypeOptionPB, FieldType::URL); -impl CellDisplayable for URLTypeOption { +impl CellDisplayable for URLTypeOptionPB { fn display_data( &self, cell_data: CellData, @@ -44,7 +44,7 @@ impl CellDisplayable for URLTypeOption { } } -impl CellDataOperation for URLTypeOption { +impl CellDataOperation for URLTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs index a1f6d4cbbf..1d3ed77389 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs @@ -1,5 +1,6 @@ use crate::entities::{ - FieldType, GridCheckboxFilter, GridDateFilter, GridNumberFilter, GridSelectOptionFilter, GridTextFilter, + CheckboxFilterConfigurationPB, DateFilterConfigurationPB, FieldType, NumberFilterConfigurationPB, + SelectOptionFilterConfigurationPB, TextFilterConfigurationPB, }; use dashmap::DashMap; use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; @@ -58,12 +59,12 @@ impl FilterResult { #[derive(Default)] pub(crate) struct FilterCache { - pub(crate) text_filter: DashMap, - pub(crate) url_filter: DashMap, - pub(crate) number_filter: DashMap, - pub(crate) date_filter: DashMap, - pub(crate) select_option_filter: DashMap, - pub(crate) checkbox_filter: DashMap, + pub(crate) text_filter: DashMap, + pub(crate) url_filter: DashMap, + pub(crate) number_filter: DashMap, + pub(crate) date_filter: DashMap, + pub(crate) select_option_filter: DashMap, + pub(crate) checkbox_filter: DashMap, } impl FilterCache { @@ -117,28 +118,34 @@ pub(crate) async fn refresh_filter_cache( let field_type: FieldType = field_rev.field_type_rev.into(); match &field_type { FieldType::RichText => { - let _ = cache.text_filter.insert(filter_id, GridTextFilter::from(filter_rev)); + let _ = cache + .text_filter + .insert(filter_id, TextFilterConfigurationPB::from(filter_rev)); } FieldType::Number => { let _ = cache .number_filter - .insert(filter_id, GridNumberFilter::from(filter_rev)); + .insert(filter_id, NumberFilterConfigurationPB::from(filter_rev)); } FieldType::DateTime => { - let _ = cache.date_filter.insert(filter_id, GridDateFilter::from(filter_rev)); + let _ = cache + .date_filter + .insert(filter_id, DateFilterConfigurationPB::from(filter_rev)); } FieldType::SingleSelect | FieldType::MultiSelect => { let _ = cache .select_option_filter - .insert(filter_id, GridSelectOptionFilter::from(filter_rev)); + .insert(filter_id, SelectOptionFilterConfigurationPB::from(filter_rev)); } FieldType::Checkbox => { let _ = cache .checkbox_filter - .insert(filter_id, GridCheckboxFilter::from(filter_rev)); + .insert(filter_id, CheckboxFilterConfigurationPB::from(filter_rev)); } FieldType::URL => { - let _ = cache.url_filter.insert(filter_id, GridTextFilter::from(filter_rev)); + let _ = cache + .url_filter + .insert(filter_id, TextFilterConfigurationPB::from(filter_rev)); } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs index 8ee5f6d99e..28f7f217e3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs @@ -3,8 +3,8 @@ use crate::entities::{FieldType, GridBlockChangesetPB}; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{AnyCellData, CellFilterOperation}; use crate::services::field::{ - CheckboxTypeOption, DateTypeOption, MultiSelectTypeOptionPB, NumberTypeOption, RichTextTypeOption, - SingleSelectTypeOptionPB, URLTypeOption, + CheckboxTypeOption, DateTypeOptionPB, MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, + SingleSelectTypeOptionPB, URLTypeOptionPB, }; use crate::services::filter::filter_cache::{ refresh_filter_cache, FilterCache, FilterId, FilterResult, FilterResultCache, @@ -182,7 +182,7 @@ fn filter_cell( FieldType::RichText => filter_cache.text_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) @@ -190,7 +190,7 @@ fn filter_cell( FieldType::Number => filter_cache.number_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) @@ -198,7 +198,7 @@ fn filter_cell( FieldType::DateTime => filter_cache.date_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) @@ -230,7 +230,7 @@ fn filter_cell( FieldType::URL => filter_cache.url_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs index 24e21bbeb7..6ff2924e16 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs @@ -1,9 +1,9 @@ -use crate::entities::{CheckboxCondition, GridCheckboxFilter}; +use crate::entities::{CheckboxCondition, CheckboxFilterConfigurationPB}; use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{CheckboxCellData, CheckboxTypeOption}; use flowy_error::FlowyResult; -impl GridCheckboxFilter { +impl CheckboxFilterConfigurationPB { pub fn is_visible(&self, cell_data: &CheckboxCellData) -> bool { let is_check = cell_data.is_check(); match self.condition { @@ -13,8 +13,8 @@ impl GridCheckboxFilter { } } -impl CellFilterOperation for CheckboxTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridCheckboxFilter) -> FlowyResult { +impl CellFilterOperation for CheckboxTypeOption { + fn apply_filter(&self, any_cell_data: AnyCellData, filter: &CheckboxFilterConfigurationPB) -> FlowyResult { if !any_cell_data.is_checkbox() { return Ok(true); } @@ -26,13 +26,13 @@ impl CellFilterOperation for CheckboxTypeOption { #[cfg(test)] mod tests { - use crate::entities::{CheckboxCondition, GridCheckboxFilter}; + use crate::entities::{CheckboxCondition, CheckboxFilterConfigurationPB}; use crate::services::field::CheckboxCellData; use std::str::FromStr; #[test] fn checkbox_filter_is_check_test() { - let checkbox_filter = GridCheckboxFilter { + let checkbox_filter = CheckboxFilterConfigurationPB { condition: CheckboxCondition::IsChecked, }; for (value, visible) in [("true", true), ("yes", true), ("false", false), ("no", false)] { @@ -43,7 +43,7 @@ mod tests { #[test] fn checkbox_filter_is_uncheck_test() { - let checkbox_filter = GridCheckboxFilter { + let checkbox_filter = CheckboxFilterConfigurationPB { condition: CheckboxCondition::IsUnChecked, }; for (value, visible) in [("false", true), ("no", true), ("true", false), ("yes", false)] { diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs index 46e2571438..18d968d2a4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs @@ -1,9 +1,9 @@ -use crate::entities::{DateFilterCondition, GridDateFilter}; +use crate::entities::{DateFilterCondition, DateFilterConfigurationPB}; use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; -use crate::services::field::{DateTimestamp, DateTypeOption}; +use crate::services::field::{DateTimestamp, DateTypeOptionPB}; use flowy_error::FlowyResult; -impl GridDateFilter { +impl DateFilterConfigurationPB { pub fn is_visible>(&self, cell_timestamp: T) -> bool { if self.start.is_none() { return false; @@ -29,8 +29,8 @@ impl GridDateFilter { } } -impl CellFilterOperation for DateTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridDateFilter) -> FlowyResult { +impl CellFilterOperation for DateTypeOptionPB { + fn apply_filter(&self, any_cell_data: AnyCellData, filter: &DateFilterConfigurationPB) -> FlowyResult { if !any_cell_data.is_date() { return Ok(true); } @@ -43,11 +43,11 @@ impl CellFilterOperation for DateTypeOption { #[cfg(test)] mod tests { #![allow(clippy::all)] - use crate::entities::{DateFilterCondition, GridDateFilter}; + use crate::entities::{DateFilterCondition, DateFilterConfigurationPB}; #[test] fn date_filter_is_test() { - let filter = GridDateFilter { + let filter = DateFilterConfigurationPB { condition: DateFilterCondition::DateIs, start: Some(123), end: None, @@ -59,7 +59,7 @@ mod tests { } #[test] fn date_filter_before_test() { - let filter = GridDateFilter { + let filter = DateFilterConfigurationPB { condition: DateFilterCondition::DateBefore, start: Some(123), end: None, @@ -71,7 +71,7 @@ mod tests { } #[test] fn date_filter_before_or_on_test() { - let filter = GridDateFilter { + let filter = DateFilterConfigurationPB { condition: DateFilterCondition::DateOnOrBefore, start: Some(123), end: None, @@ -83,7 +83,7 @@ mod tests { } #[test] fn date_filter_after_test() { - let filter = GridDateFilter { + let filter = DateFilterConfigurationPB { condition: DateFilterCondition::DateAfter, start: Some(123), end: None, @@ -95,7 +95,7 @@ mod tests { } #[test] fn date_filter_within_test() { - let filter = GridDateFilter { + let filter = DateFilterConfigurationPB { condition: DateFilterCondition::DateWithIn, start: Some(123), end: Some(130), diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs index f44c1d2d62..45ae0ac464 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs @@ -1,12 +1,12 @@ -use crate::entities::{GridNumberFilter, NumberFilterCondition}; +use crate::entities::{NumberFilterCondition, NumberFilterConfigurationPB}; use crate::services::cell::{AnyCellData, CellFilterOperation}; -use crate::services::field::{NumberCellData, NumberTypeOption}; +use crate::services::field::{NumberCellData, NumberTypeOptionPB}; use flowy_error::FlowyResult; use rust_decimal::prelude::Zero; use rust_decimal::Decimal; use std::str::FromStr; -impl GridNumberFilter { +impl NumberFilterConfigurationPB { pub fn is_visible(&self, num_cell_data: &NumberCellData) -> bool { if self.content.is_none() { return false; @@ -31,8 +31,8 @@ impl GridNumberFilter { } } -impl CellFilterOperation for NumberTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridNumberFilter) -> FlowyResult { +impl CellFilterOperation for NumberTypeOptionPB { + fn apply_filter(&self, any_cell_data: AnyCellData, filter: &NumberFilterConfigurationPB) -> FlowyResult { if !any_cell_data.is_number() { return Ok(true); } @@ -46,11 +46,11 @@ impl CellFilterOperation for NumberTypeOption { #[cfg(test)] mod tests { - use crate::entities::{GridNumberFilter, NumberFilterCondition}; + use crate::entities::{NumberFilterCondition, NumberFilterConfigurationPB}; use crate::services::field::{NumberCellData, NumberFormat}; #[test] fn number_filter_equal_test() { - let number_filter = GridNumberFilter { + let number_filter = NumberFilterConfigurationPB { condition: NumberFilterCondition::Equal, content: Some("123".to_owned()), }; @@ -68,7 +68,7 @@ mod tests { } #[test] fn number_filter_greater_than_test() { - let number_filter = GridNumberFilter { + let number_filter = NumberFilterConfigurationPB { condition: NumberFilterCondition::GreaterThan, content: Some("12".to_owned()), }; @@ -80,7 +80,7 @@ mod tests { #[test] fn number_filter_less_than_test() { - let number_filter = GridNumberFilter { + let number_filter = NumberFilterConfigurationPB { condition: NumberFilterCondition::LessThan, content: Some("100".to_owned()), }; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs index 5ae8210746..f48069911e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs @@ -1,12 +1,12 @@ #![allow(clippy::needless_collect)] -use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; +use crate::entities::{SelectOptionCondition, SelectOptionFilterConfigurationPB}; use crate::services::cell::{AnyCellData, CellFilterOperation}; use crate::services::field::{MultiSelectTypeOptionPB, SingleSelectTypeOptionPB}; use crate::services::field::{SelectOptionOperation, SelectedSelectOptions}; use flowy_error::FlowyResult; -impl GridSelectOptionFilter { +impl SelectOptionFilterConfigurationPB { pub fn is_visible(&self, selected_options: &SelectedSelectOptions) -> bool { let selected_option_ids: Vec<&String> = selected_options.options.iter().map(|option| &option.id).collect(); match self.condition { @@ -39,8 +39,12 @@ impl GridSelectOptionFilter { } } -impl CellFilterOperation for MultiSelectTypeOptionPB { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult { +impl CellFilterOperation for MultiSelectTypeOptionPB { + fn apply_filter( + &self, + any_cell_data: AnyCellData, + filter: &SelectOptionFilterConfigurationPB, + ) -> FlowyResult { if !any_cell_data.is_multi_select() { return Ok(true); } @@ -50,8 +54,12 @@ impl CellFilterOperation for MultiSelectTypeOptionPB { } } -impl CellFilterOperation for SingleSelectTypeOptionPB { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult { +impl CellFilterOperation for SingleSelectTypeOptionPB { + fn apply_filter( + &self, + any_cell_data: AnyCellData, + filter: &SelectOptionFilterConfigurationPB, + ) -> FlowyResult { if !any_cell_data.is_single_select() { return Ok(true); } @@ -63,7 +71,7 @@ impl CellFilterOperation for SingleSelectTypeOptionPB { #[cfg(test)] mod tests { #![allow(clippy::all)] - use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; + use crate::entities::{SelectOptionCondition, SelectOptionFilterConfigurationPB}; use crate::services::field::selection_type_option::{SelectOptionPB, SelectedSelectOptions}; #[test] @@ -72,7 +80,7 @@ mod tests { let option_2 = SelectOptionPB::new("B"); let option_3 = SelectOptionPB::new("C"); - let filter_1 = GridSelectOptionFilter { + let filter_1 = SelectOptionFilterConfigurationPB { condition: SelectOptionCondition::OptionIs, option_ids: vec![option_1.id.clone(), option_2.id.clone()], }; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs index 25f3902ceb..86cb2aadfa 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs @@ -1,9 +1,9 @@ -use crate::entities::{GridTextFilter, TextFilterCondition}; +use crate::entities::{TextFilterCondition, TextFilterConfigurationPB}; use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; -use crate::services::field::{RichTextTypeOption, TextCellData}; +use crate::services::field::{RichTextTypeOptionPB, TextCellData}; use flowy_error::FlowyResult; -impl GridTextFilter { +impl TextFilterConfigurationPB { pub fn is_visible>(&self, cell_data: T) -> bool { let cell_data = cell_data.as_ref(); let s = cell_data.to_lowercase(); @@ -24,8 +24,8 @@ impl GridTextFilter { } } -impl CellFilterOperation for RichTextTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridTextFilter) -> FlowyResult { +impl CellFilterOperation for RichTextTypeOptionPB { + fn apply_filter(&self, any_cell_data: AnyCellData, filter: &TextFilterConfigurationPB) -> FlowyResult { if !any_cell_data.is_text() { return Ok(true); } @@ -38,11 +38,11 @@ impl CellFilterOperation for RichTextTypeOption { #[cfg(test)] mod tests { #![allow(clippy::all)] - use crate::entities::{GridTextFilter, TextFilterCondition}; + use crate::entities::{TextFilterCondition, TextFilterConfigurationPB}; #[test] fn text_filter_equal_test() { - let text_filter = GridTextFilter { + let text_filter = TextFilterConfigurationPB { condition: TextFilterCondition::Is, content: Some("appflowy".to_owned()), }; @@ -54,7 +54,7 @@ mod tests { } #[test] fn text_filter_start_with_test() { - let text_filter = GridTextFilter { + let text_filter = TextFilterConfigurationPB { condition: TextFilterCondition::StartsWith, content: Some("appflowy".to_owned()), }; @@ -66,7 +66,7 @@ mod tests { #[test] fn text_filter_end_with_test() { - let text_filter = GridTextFilter { + let text_filter = TextFilterConfigurationPB { condition: TextFilterCondition::EndsWith, content: Some("appflowy".to_owned()), }; @@ -77,7 +77,7 @@ mod tests { } #[test] fn text_filter_empty_test() { - let text_filter = GridTextFilter { + let text_filter = TextFilterConfigurationPB { condition: TextFilterCondition::TextIsEmpty, content: Some("appflowy".to_owned()), }; @@ -87,7 +87,7 @@ mod tests { } #[test] fn text_filter_contain_test() { - let text_filter = GridTextFilter { + let text_filter = TextFilterConfigurationPB { condition: TextFilterCondition::Contains, content: Some("appflowy".to_owned()), }; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs index 15254d4713..4f0a7b93cd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs @@ -1,10 +1,10 @@ -use crate::entities::GridTextFilter; +use crate::entities::TextFilterConfigurationPB; use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; -use crate::services::field::{TextCellData, URLTypeOption}; +use crate::services::field::{TextCellData, URLTypeOptionPB}; use flowy_error::FlowyResult; -impl CellFilterOperation for URLTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridTextFilter) -> FlowyResult { +impl CellFilterOperation for URLTypeOptionPB { + fn apply_filter(&self, any_cell_data: AnyCellData, filter: &TextFilterConfigurationPB) -> FlowyResult { if !any_cell_data.is_url() { return Ok(true); } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 4654f8722c..b0cab2f5af 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -452,7 +452,7 @@ impl GridRevisionEditor { pub async fn get_grid_setting(&self) -> FlowyResult { let read_guard = self.grid_pad.read().await; - let grid_setting_rev = read_guard.get_grid_setting_rev(); + let grid_setting_rev = read_guard.get_setting_rev(); let field_revs = read_guard.get_field_revs(None)?; let grid_setting = make_grid_setting(grid_setting_rev, &field_revs); Ok(grid_setting) @@ -564,8 +564,9 @@ impl GridRevisionEditor { }) } - pub async fn get_group(&self) -> FlowyResult { - todo!() + pub async fn load_groups(&self) -> FlowyResult { + let groups = self.group_service.load_groups().await.unwrap_or(vec![]); + Ok(RepeatedGridGroupPB { items: groups }) } async fn modify(&self, f: F) -> FlowyResult<()> diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 50d04febf4..5bafd5b841 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,5 +1,13 @@ +use crate::entities::{ + CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, NumberGroupConfigurationPB, + SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, +}; use crate::services::block_manager::GridBlockManager; +use crate::services::cell::{decode_any_cell_data, CellBytes}; use crate::services::grid_editor_task::GridServiceTaskScheduler; +use bytes::Bytes; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, GroupConfigurationRevision, RowRevision}; use flowy_sync::client_grid::GridRevisionPad; use std::sync::Arc; use tokio::sync::RwLock; @@ -26,4 +34,116 @@ impl GridGroupService { block_manager, } } + + pub(crate) async fn load_groups(&self) -> Option> { + let grid_pad = self.grid_pad.read().await; + let field_rev = find_group_field(grid_pad.fields())?; + let field_type: FieldType = field_rev.field_type_rev.clone().into(); + let setting = grid_pad.get_setting_rev(); + let mut configurations = setting.get_groups(&setting.layout, &field_rev.id, &field_rev.field_type_rev)?; + + if configurations.is_empty() { + return None; + } + assert_eq!(configurations.len(), 1); + let configuration = (&*configurations.pop().unwrap()).clone(); + + let blocks = self.block_manager.get_block_snapshots(None).await.unwrap(); + + let row_revs = blocks + .into_iter() + .map(|block| block.row_revs) + .flatten() + .collect::>>(); + + let groups = match field_type { + FieldType::RichText => { + let generator = GroupGenerator::::from_configuration(configuration); + } + FieldType::Number => { + let generator = GroupGenerator::::from_configuration(configuration); + } + FieldType::DateTime => { + let generator = GroupGenerator::::from_configuration(configuration); + } + FieldType::SingleSelect => { + let generator = GroupGenerator::::from_configuration(configuration); + } + FieldType::MultiSelect => { + let generator = GroupGenerator::::from_configuration(configuration); + } + FieldType::Checkbox => { + let generator = GroupGenerator::::from_configuration(configuration); + } + FieldType::URL => { + let generator = GroupGenerator::::from_configuration(configuration); + } + }; + None + } +} + +pub struct GroupGenerator { + field_id: String, + groups: Vec, + configuration: Option, +} + +pub struct Group { + row_ids: Vec, + content: String, +} + +impl GroupGenerator +where + T: TryFrom, +{ + pub fn from_configuration(configuration: GroupConfigurationRevision) -> FlowyResult { + let bytes = Bytes::from(configuration.content.unwrap_or(vec![])); + Self::from_bytes(&configuration.field_id, bytes) + } + + pub fn from_bytes(field_id: &str, bytes: Bytes) -> FlowyResult { + let configuration = if bytes.is_empty() { + None + } else { + Some(T::try_from(bytes)?) + }; + Ok(Self { + field_id: field_id.to_owned(), + groups: vec![], + configuration, + }) + } +} +pub trait GroupConfiguration { + fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool; +} + +impl GroupGenerator +where + T: GroupConfiguration, +{ + pub fn group_row(&mut self, field_rev: &Arc, row: &RowRevision) { + if self.configuration.is_none() { + return; + } + let configuration = self.configuration.as_ref().unwrap(); + if let Some(cell_rev) = row.cells.get(&self.field_id) { + for group in self.groups.iter_mut() { + let cell_rev: CellRevision = cell_rev.clone(); + let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev); + if configuration.should_group(&group.content, cell_bytes) { + group.row_ids.push(row.id.clone()); + } + } + } + } +} + +fn find_group_field(field_revs: &[Arc]) -> Option<&Arc> { + field_revs.iter().find(|field_rev| { + let field_type: FieldType = field_rev.field_type_rev.into(); + field_type.can_be_group() + }) } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs new file mode 100644 index 0000000000..19a79428c3 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs @@ -0,0 +1,17 @@ +use crate::entities::CheckboxGroupConfigurationPB; +use crate::services::cell::{AnyCellData, CellData, CellGroupOperation}; +use crate::services::field::{CheckboxCellData, CheckboxTypeOption}; +use flowy_error::FlowyResult; + +impl CellGroupOperation for CheckboxTypeOption { + fn apply_group(&self, any_cell_data: AnyCellData, content: &str) -> FlowyResult { + if !any_cell_data.is_checkbox() { + return Ok(true); + } + let cell_data: CellData = any_cell_data.into(); + let checkbox_cell_data = cell_data.try_into_inner()?; + + // Ok(checkbox_cell_data.as_ref() == content) + todo!() + } +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_sort.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/date_group.rs similarity index 100% rename from shared-lib/flowy-grid-data-model/src/revision/grid_sort.rs rename to frontend/rust-lib/flowy-grid/src/services/group/impls/date_group.rs diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs new file mode 100644 index 0000000000..37babdb470 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs @@ -0,0 +1,11 @@ +mod checkbox_group; +mod date_group; +mod number_group; +mod select_option_group; +mod text_group; + +pub use checkbox_group::*; +pub use date_group::*; +pub use number_group::*; +pub use select_option_group::*; +pub use text_group::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/number_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/number_group.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs new file mode 100644 index 0000000000..b9e8f5b53b --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs @@ -0,0 +1,8 @@ +use crate::entities::SelectOptionGroupConfigurationPB; +use crate::services::field::SelectedSelectOptions; + +impl SelectOptionGroupConfigurationPB { + pub fn is_visible(&self, selected_options: &SelectedSelectOptions) -> bool { + return true; + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/text_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/text_group.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/url_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/url_group.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index 9fe58d6fc1..f30f63b452 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,3 +1,5 @@ mod group_service; +mod impls; pub(crate) use group_service::*; +pub(crate) use impls::*; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index 900a41c6ed..b25bb044fc 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -170,7 +170,7 @@ impl GridRowTest { FieldType::Number => { let field_rev = self.editor.get_field_rev(&cell_id.field_id).await.unwrap(); let number_type_option = field_rev - .get_type_option_entry::(FieldType::Number.into()) + .get_type_option_entry::(FieldType::Number.into()) .unwrap(); let cell_data = self .editor diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs index bfd8c92f0a..8615d868c2 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs @@ -12,7 +12,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { let cloned_field_rev = field_rev.clone(); let type_option_data = field_rev - .get_type_option_entry::(field_rev.field_type_rev) + .get_type_option_entry::(field_rev.field_type_rev) .unwrap() .protobuf_bytes() .to_vec(); diff --git a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs index 06e7971cf0..7079b52229 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] -pub struct GridFilterRevision { +pub struct FilterConfigurationRevision { pub id: String, pub field_id: String, pub condition: u8, diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs index 268682d552..dae56bd123 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs @@ -1,8 +1,10 @@ +use crate::revision::FieldTypeRevision; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct GridGroupRevision { +pub struct GroupConfigurationRevision { pub id: String, pub field_id: String, - pub sub_field_id: Option, + pub field_type_rev: FieldTypeRevision, + pub content: Option>, } diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index 09056f827d..b60baff811 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -171,6 +171,7 @@ impl FieldRevision { pub fn get_type_option_entry(&self, field_type_rev: FieldTypeRevision) -> Option { let id = field_type_rev.to_string(); + // TODO: cache the deserialized type option self.type_options.get(&id).map(|s| T::from_json_str(s)) } diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 277de36d62..6181ca8195 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -1,5 +1,5 @@ -use crate::revision::filter_rev::GridFilterRevision; -use crate::revision::grid_group::GridGroupRevision; +use crate::revision::filter_rev::FilterConfigurationRevision; +use crate::revision::grid_group::GroupConfigurationRevision; use crate::revision::{FieldRevision, FieldTypeRevision}; use indexmap::IndexMap; use nanoid::nanoid; @@ -21,29 +21,29 @@ pub fn gen_grid_sort_id() -> String { nanoid!(6) } -pub type GridFilters = SettingContainer; -pub type GridFilterRevisionMap = GridObjectRevisionMap; -pub type FiltersByFieldId = HashMap>>; +pub type FilterConfigurations = SettingConfiguration; +pub type FilterConfigurationRevisionMap = GridObjectRevisionMap; +pub type FilterConfigurationsByFieldId = HashMap>>; // -pub type GridGroups = SettingContainer; -pub type GridGroupRevisionMap = GridObjectRevisionMap; -pub type GroupsByFieldId = HashMap>>; +pub type GroupConfigurations = SettingConfiguration; +pub type GroupConfigurationRevisionMap = GridObjectRevisionMap; +pub type GroupConfigurationsByFieldId = HashMap>>; // -pub type GridSorts = SettingContainer; -pub type GridSortRevisionMap = GridObjectRevisionMap; -pub type SortsByFieldId = HashMap>>; +pub type SortConfigurations = SettingConfiguration; +pub type SortConfigurationRevisionMap = GridObjectRevisionMap; +pub type SortConfigurationsByFieldId = HashMap>>; #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] pub struct GridSettingRevision { pub layout: GridLayoutRevision, - pub filters: GridFilters, + pub filters: FilterConfigurations, #[serde(default)] - pub groups: GridGroups, + pub groups: GroupConfigurations, #[serde(skip)] - pub sorts: GridSorts, + pub sorts: SortConfigurations, } #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] @@ -67,7 +67,7 @@ impl std::default::Default for GridLayoutRevision { } impl GridSettingRevision { - pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { + pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { self.groups.get_all_objects(&self.layout, field_revs) } @@ -76,7 +76,7 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type_rev: &FieldTypeRevision, - ) -> Option>> { + ) -> Option>> { self.groups.get_objects(layout, field_id, field_type_rev) } @@ -85,7 +85,7 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { + ) -> Option<&mut Vec>> { self.groups.get_mut_objects(layout, field_id, field_type) } @@ -94,12 +94,13 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, - group_rev: GridGroupRevision, + group_rev: GroupConfigurationRevision, ) { + self.groups.remove_all(layout); self.groups.insert_object(layout, field_id, field_type, group_rev); } - pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { + pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { self.filters.get_all_objects(&self.layout, field_revs) } @@ -108,7 +109,7 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type_rev: &FieldTypeRevision, - ) -> Option>> { + ) -> Option>> { self.filters.get_objects(layout, field_id, field_type_rev) } @@ -117,7 +118,7 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { + ) -> Option<&mut Vec>> { self.filters.get_mut_objects(layout, field_id, field_type) } @@ -126,25 +127,25 @@ impl GridSettingRevision { layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, - filter_rev: GridFilterRevision, + filter_rev: FilterConfigurationRevision, ) { self.filters.insert_object(layout, field_id, field_type, filter_rev); } - pub fn get_all_sort(&self) -> Option { + pub fn get_all_sort(&self) -> Option { None } } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct GridSortRevision { +pub struct SortConfigurationRevision { pub id: String, pub field_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] #[serde(transparent)] -pub struct SettingContainer +pub struct SettingConfiguration where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { @@ -157,7 +158,7 @@ where inner: IndexMap>>, } -impl SettingContainer +impl SettingConfiguration where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { @@ -229,6 +230,12 @@ where .or_insert_with(Vec::new) .push(Arc::new(object)) } + + pub fn remove_all(&mut self, layout: &GridLayoutRevision) { + if let Some(object_rev_map_by_field_id) = self.inner.get_mut(layout) { + object_rev_map_by_field_id.clear() + } + } } #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] diff --git a/shared-lib/flowy-grid-data-model/src/revision/mod.rs b/shared-lib/flowy-grid-data-model/src/revision/mod.rs index 9736442557..67b58978b5 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/mod.rs @@ -2,10 +2,8 @@ mod filter_rev; mod grid_group; mod grid_rev; mod grid_setting_rev; -mod grid_sort; pub use filter_rev::*; pub use grid_group::*; pub use grid_rev::*; pub use grid_setting_rev::*; -pub use grid_sort::*; diff --git a/shared-lib/flowy-grid-data-model/tests/serde_test.rs b/shared-lib/flowy-grid-data-model/tests/serde_test.rs index 1dff913547..8a630a4651 100644 --- a/shared-lib/flowy-grid-data-model/tests/serde_test.rs +++ b/shared-lib/flowy-grid-data-model/tests/serde_test.rs @@ -8,6 +8,6 @@ fn grid_default_serde_test() { let json = serde_json::to_string(&grid).unwrap(); assert_eq!( json, - r#"{"grid_id":"1","fields":[],"blocks":[],"setting":{"layout":0,"filters":[]}}"# + r#"{"grid_id":"1","fields":[],"blocks":[],"setting":{"layout":0,"filters":[],"groups":[]}}"# ) } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index 73c32834b1..a762103e57 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -6,9 +6,9 @@ use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_delta_from_revisions}; use bytes::Bytes; use flowy_grid_data_model::revision::{ - gen_block_id, gen_grid_filter_id, gen_grid_group_id, gen_grid_id, gen_grid_sort_id, FieldRevision, - FieldTypeRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, GridFilterRevision, GridGroupRevision, - GridLayoutRevision, GridRevision, GridSettingRevision, GridSortRevision, + gen_block_id, gen_grid_filter_id, gen_grid_group_id, gen_grid_id, FieldRevision, FieldTypeRevision, + FilterConfigurationRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, GridLayoutRevision, + GridRevision, GridSettingRevision, GroupConfigurationRevision, }; use lib_infra::util::move_vec_element; use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; @@ -341,7 +341,7 @@ impl GridRevisionPad { }) } - pub fn get_grid_setting_rev(&self) -> &GridSettingRevision { + pub fn get_setting_rev(&self) -> &GridSettingRevision { &self.grid_rev.setting } @@ -350,7 +350,7 @@ impl GridRevisionPad { &self, layout: Option<&GridLayoutRevision>, field_ids: Option>, - ) -> Option>> { + ) -> Option>> { let mut filter_revs = vec![]; let layout_ty = layout.unwrap_or(&self.grid_rev.setting.layout); let field_revs = self.get_field_revs(None).ok()?; @@ -419,7 +419,7 @@ impl GridRevisionPad { groups.retain(|filter| filter.id != params.group_id); } } - if let Some(sort) = changeset.insert_sort { + if let Some(_sort) = changeset.insert_sort { // let rev = GridSortRevision { // id: gen_grid_sort_id(), // field_id: sort.field_id, @@ -434,7 +434,7 @@ impl GridRevisionPad { is_changed = Some(()) } - if let Some(delete_sort_id) = changeset.delete_sort { + if let Some(_delete_sort_id) = changeset.delete_sort { // match grid_rev.setting.sorts.get_mut(&layout_rev) { // Some(sorts) => sorts.retain(|sort| sort.id != delete_sort_id), // None => { @@ -559,19 +559,20 @@ impl std::default::Default for GridRevisionPad { } } -fn make_filter_revision(params: &CreateGridFilterParams) -> GridFilterRevision { - GridFilterRevision { +fn make_filter_revision(params: &CreateGridFilterParams) -> FilterConfigurationRevision { + FilterConfigurationRevision { id: gen_grid_filter_id(), field_id: params.field_id.clone(), - condition: params.condition.clone(), + condition: params.condition, content: params.content.clone(), } } -fn make_group_revision(params: &CreateGridGroupParams) -> GridGroupRevision { - GridGroupRevision { +fn make_group_revision(params: &CreateGridGroupParams) -> GroupConfigurationRevision { + GroupConfigurationRevision { id: gen_grid_group_id(), field_id: params.field_id.clone(), - sub_field_id: params.sub_field_id.clone(), + field_type_rev: params.field_type_rev.clone(), + content: params.content.clone(), } } diff --git a/shared-lib/flowy-sync/src/entities/grid.rs b/shared-lib/flowy-sync/src/entities/grid.rs index 8b5e9d4629..23c8658839 100644 --- a/shared-lib/flowy-sync/src/entities/grid.rs +++ b/shared-lib/flowy-sync/src/entities/grid.rs @@ -31,8 +31,8 @@ pub struct DeleteFilterParams { pub struct CreateGridGroupParams { pub field_id: String, - pub sub_field_id: Option, pub field_type_rev: FieldTypeRevision, + pub content: Option>, } pub struct DeleteGroupParams { From f637839cfefcf400b01005fe40dc364c0e5ffda7 Mon Sep 17 00:00:00 2001 From: Victor Teles Date: Thu, 11 Aug 2022 10:39:32 -0300 Subject: [PATCH 085/224] refactor: fixed lint warnings --- .../app_flowy/lib/workspace/presentation/home/home_stack.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart index a64fc8b5b5..73ebde5141 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_stack.dart @@ -1,13 +1,9 @@ -import 'dart:io' show Platform; - import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/plugins/blank/blank.dart'; import 'package:app_flowy/workspace/presentation/home/toast.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:time/time.dart'; import 'package:app_flowy/startup/plugin/plugin.dart'; From 2fddbce18cd222980034b3de4b181906980e168c Mon Sep 17 00:00:00 2001 From: Binlogo Date: Thu, 11 Aug 2022 23:35:07 +0800 Subject: [PATCH 086/224] chore: speed up ci by respecting the cargo deps cache --- .github/workflows/ci.yaml | 32 +++++++++++++--------- .github/workflows/dart_lint.yml | 35 +++++++++++++++++++----- .github/workflows/dart_test.yml | 47 +++++++++++++++++++-------------- 3 files changed, 74 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46df373824..515b3af226 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,11 +3,11 @@ name: CI on: push: branches: - - 'main' - + - "main" + pull_request: branches: - - 'main' + - "main" jobs: build: @@ -23,36 +23,37 @@ jobs: steps: - uses: actions/checkout@v2 - + - id: rust_toolchain uses: actions-rs/toolchain@v1 with: - toolchain: 'stable-2022-01-20' - + toolchain: "stable-2022-01-20" + - id: flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" cache: true - flutter-version: '3.0.5' + flutter-version: "3.0.5" - name: Cache Cargo + id: cache-cargo uses: actions/cache@v2 - with: + with: path: | ~/.cargo key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} - name: Cache Rust uses: actions/cache@v2 - with: + with: path: | frontend/rust-lib/target shared-lib/target - key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} + key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} - name: Setup Environment - run: | + run: | if [ "$RUNNER_OS" == "Linux" ]; then sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list @@ -63,11 +64,16 @@ jobs: fi shell: bash - - name: Deps + - if: steps.cache-cargo.outputs.cache-hit != 'true' + name: Deps working-directory: frontend run: | cargo install cargo-make cargo install duckscript_cli + + - name: Cargo make flowy_dev + working-directory: frontend + run: | cargo make flowy_dev - name: Config Flutter diff --git a/.github/workflows/dart_lint.yml b/.github/workflows/dart_lint.yml index 3ff87dc680..251848fb31 100644 --- a/.github/workflows/dart_lint.yml +++ b/.github/workflows/dart_lint.yml @@ -7,14 +7,14 @@ name: Flutter lint on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] env: CARGO_TERM_COLOR: always -jobs: +jobs: flutter-analyze: name: flutter analyze runs-on: ubuntu-latest @@ -23,16 +23,38 @@ jobs: uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 with: - flutter-version: '3.0.5' + flutter-version: "3.0.5" channel: "stable" - uses: actions-rs/toolchain@v1 with: - toolchain: 'stable-2022-01-20' + toolchain: "stable-2022-01-20" - - name: Rust Deps + - name: Cache Cargo + id: cache-cargo + uses: actions/cache@v2 + with: + path: | + ~/.cargo + key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} + + - name: Cache Rust + id: cache-rust-target + uses: actions/cache@v2 + with: + path: | + frontend/rust-lib/target + shared-lib/target + key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} + + - if: steps.cache-cargo.outputs.cache-hit != 'true' + name: Rust Deps working-directory: frontend run: | cargo install cargo-make + + - name: Cargo make flowy dev + working-directory: frontend + run: | cargo make flowy_dev - name: Flutter Deps @@ -53,4 +75,3 @@ jobs: - name: Run Flutter Analyzer working-directory: frontend/app_flowy run: flutter analyze - diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml index e2a858999c..3ff581c2a3 100644 --- a/.github/workflows/dart_test.yml +++ b/.github/workflows/dart_test.yml @@ -3,12 +3,12 @@ name: Unit test(Flutter) on: push: branches: - - 'main' - + - "main" + pull_request: branches: - - 'main' - - 'feat/flowy_editor' + - "main" + - "feat/flowy_editor" env: CARGO_TERM_COLOR: always @@ -18,42 +18,49 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - + - uses: actions-rs/toolchain@v1 with: - toolchain: 'stable-2022-01-20' - + toolchain: "stable-2022-01-20" + - uses: subosito/flutter-action@v2 with: - channel: 'stable' - flutter-version: '3.0.5' + channel: "stable" + flutter-version: "3.0.5" cache: true - name: Cache Cargo uses: actions/cache@v2 - with: + with: path: | ~/.cargo key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} - name: Cache Rust + id: cache-rust-target uses: actions/cache@v2 - with: + with: path: | frontend/rust-lib/target shared-lib/target - key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} + key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }} + + - if: steps.cache-cargo.outputs.cache-hit != 'true' + name: Rust Deps + working-directory: frontend + run: | + cargo install cargo-make + + - name: Cargo make flowy dev + working-directory: frontend + run: | + cargo make flowy_dev - name: Flutter Deps working-directory: frontend/app_flowy run: | flutter config --enable-linux-desktop - - - name: Rust Deps - working-directory: frontend - run: | - cargo install cargo-make - cargo make flowy_dev + - name: Build FlowySDK working-directory: frontend run: | @@ -65,7 +72,7 @@ jobs: flutter packages pub get flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json flutter packages pub run build_runner build --delete-conflicting-outputs - + - name: Run bloc tests working-directory: frontend/app_flowy run: | @@ -76,4 +83,4 @@ jobs: working-directory: frontend/app_flowy/packages/flowy_editor run: | flutter pub get - flutter test \ No newline at end of file + flutter test From 707ddb4e732735e48481f973953932ad24fa0fbd Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 09:49:07 +0800 Subject: [PATCH 087/224] chore: generic GroupGenerator --- .../src/services/cell/cell_operation.rs | 4 +- .../src/services/field/field_builder.rs | 2 +- .../checkbox_type_option/checkbox_tests.rs | 4 +- .../checkbox_type_option.rs | 12 +-- .../src/services/filter/filter_service.rs | 4 +- .../services/filter/impls/checkbox_filter.rs | 4 +- .../src/services/group/group_configuration.rs | 70 +++++++++++++++ .../group/group_generator/checkbox_group.rs | 7 ++ .../group/group_generator/date_group.rs | 5 ++ .../group/group_generator/generator.rs | 88 +++++++++++++++++++ .../group/{impls => group_generator}/mod.rs | 4 + .../group/group_generator/number_group.rs | 5 ++ .../group_generator/select_option_group.rs | 44 ++++++++++ .../group/group_generator/text_group.rs | 5 ++ .../group/group_generator/url_group.rs | 5 ++ .../src/services/group/group_service.rs | 80 ++++------------- .../services/group/impls/checkbox_group.rs | 17 ---- .../src/services/group/impls/date_group.rs | 0 .../src/services/group/impls/number_group.rs | 0 .../group/impls/select_option_group.rs | 8 -- .../src/services/group/impls/text_group.rs | 0 .../src/services/group/impls/url_group.rs | 0 .../flowy-grid/src/services/group/mod.rs | 6 +- 23 files changed, 267 insertions(+), 107 deletions(-) create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs rename frontend/rust-lib/flowy-grid/src/services/group/{impls => group_generator}/mod.rs (75%) create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/date_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/number_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/text_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/impls/url_group.rs diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 7ebee15d53..4a12bfd613 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -66,7 +66,7 @@ pub fn apply_cell_data_changeset>( SingleSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev) } FieldType::MultiSelect => MultiSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), - FieldType::Checkbox => CheckboxTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::Checkbox => CheckboxTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::URL => URLTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev), }?; @@ -116,7 +116,7 @@ pub fn try_decode_cell_data( .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::Checkbox => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::URL => field_rev .get_type_option_entry::(field_type)? diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs index 52f23d4906..efa0aec03d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs @@ -95,7 +95,7 @@ pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box DateTypeOptionPB::default().into(), FieldType::SingleSelect => SingleSelectTypeOptionPB::default().into(), FieldType::MultiSelect => MultiSelectTypeOptionPB::default().into(), - FieldType::Checkbox => CheckboxTypeOption::default().into(), + FieldType::Checkbox => CheckboxTypeOptionPB::default().into(), FieldType::URL => URLTypeOptionPB::default().into(), }; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs index ddd1ba049d..838e341666 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs @@ -8,7 +8,7 @@ mod tests { #[test] fn checkout_box_description_test() { - let type_option = CheckboxTypeOption::default(); + let type_option = CheckboxTypeOptionPB::default(); let field_type = FieldType::Checkbox; let field_rev = FieldBuilder::from_field_type(&field_type).build(); @@ -27,7 +27,7 @@ mod tests { } fn assert_checkbox( - type_option: &CheckboxTypeOption, + type_option: &CheckboxTypeOptionPB, input_str: &str, expected_str: &str, field_type: &FieldType, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index 155965f409..cf668fcc1e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; #[derive(Default)] -pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption); +pub struct CheckboxTypeOptionBuilder(CheckboxTypeOptionPB); impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption); +impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOptionPB); impl CheckboxTypeOptionBuilder { pub fn set_selected(mut self, is_selected: bool) -> Self { @@ -32,13 +32,13 @@ impl TypeOptionBuilder for CheckboxTypeOptionBuilder { } #[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] -pub struct CheckboxTypeOption { +pub struct CheckboxTypeOptionPB { #[pb(index = 1)] pub is_selected: bool, } -impl_type_option!(CheckboxTypeOption, FieldType::Checkbox); +impl_type_option!(CheckboxTypeOptionPB, FieldType::Checkbox); -impl CellDisplayable for CheckboxTypeOption { +impl CellDisplayable for CheckboxTypeOptionPB { fn display_data( &self, cell_data: CellData, @@ -50,7 +50,7 @@ impl CellDisplayable for CheckboxTypeOption { } } -impl CellDataOperation for CheckboxTypeOption { +impl CellDataOperation for CheckboxTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs index 28f7f217e3..c9850d1c6c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs @@ -3,7 +3,7 @@ use crate::entities::{FieldType, GridBlockChangesetPB}; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{AnyCellData, CellFilterOperation}; use crate::services::field::{ - CheckboxTypeOption, DateTypeOptionPB, MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, + CheckboxTypeOptionPB, DateTypeOptionPB, MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, URLTypeOptionPB, }; use crate::services::filter::filter_cache::{ @@ -222,7 +222,7 @@ fn filter_cell( FieldType::Checkbox => filter_cache.checkbox_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs index 6ff2924e16..3239ff449d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs @@ -1,6 +1,6 @@ use crate::entities::{CheckboxCondition, CheckboxFilterConfigurationPB}; use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; -use crate::services::field::{CheckboxCellData, CheckboxTypeOption}; +use crate::services::field::{CheckboxCellData, CheckboxTypeOptionPB}; use flowy_error::FlowyResult; impl CheckboxFilterConfigurationPB { @@ -13,7 +13,7 @@ impl CheckboxFilterConfigurationPB { } } -impl CellFilterOperation for CheckboxTypeOption { +impl CellFilterOperation for CheckboxTypeOptionPB { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &CheckboxFilterConfigurationPB) -> FlowyResult { if !any_cell_data.is_checkbox() { return Ok(true); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs new file mode 100644 index 0000000000..b67e4f5404 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs @@ -0,0 +1,70 @@ +use crate::entities::{ + CheckboxGroupConfigurationPB, DateGroupConfigurationPB, NumberGroupConfigurationPB, + SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, +}; +use crate::services::cell::CellBytes; +use crate::services::field::{ + CheckboxCellDataParser, DateCellDataParser, NumberCellDataParser, NumberFormat, SelectOptionCellDataParser, + TextCellDataParser, URLCellDataParser, +}; +use crate::services::group::GroupAction; + +// impl GroupAction for TextGroupConfigurationPB { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// if let Ok(cell_data) = cell_bytes.with_parser(TextCellDataParser()) { +// cell_data.as_ref() == content +// } else { +// false +// } +// } +// } +// +// impl GroupAction for NumberGroupConfigurationPB { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// if let Ok(cell_data) = cell_bytes.with_parser(NumberCellDataParser(NumberFormat::Num)) { +// false +// } else { +// false +// } +// } +// } +// +// impl GroupAction for DateGroupConfigurationPB { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// if let Ok(cell_data) = cell_bytes.with_parser(DateCellDataParser()) { +// false +// } else { +// false +// } +// } +// } +// +// impl GroupAction for SelectOptionGroupConfigurationPB { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// if let Ok(cell_data) = cell_bytes.with_parser(SelectOptionCellDataParser()) { +// false +// } else { +// false +// } +// } +// } +// +// impl GroupAction for UrlGroupConfigurationPB { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// if let Ok(cell_data) = cell_bytes.with_parser(URLCellDataParser()) { +// false +// } else { +// false +// } +// } +// } +// +// impl GroupAction for CheckboxGroupConfigurationPB { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// if let Ok(cell_data) = cell_bytes.with_parser(CheckboxCellDataParser()) { +// false +// } else { +// false +// } +// } +// } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs new file mode 100644 index 0000000000..43df16d196 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -0,0 +1,7 @@ +use crate::entities::CheckboxGroupConfigurationPB; +use crate::services::cell::{AnyCellData, CellData, CellGroupOperation}; +use crate::services::field::{CheckboxCellData, CheckboxTypeOptionPB}; +use crate::services::group::GroupController; +use flowy_error::FlowyResult; + +// pub type CheckboxGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs new file mode 100644 index 0000000000..d0cda990d3 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs @@ -0,0 +1,5 @@ +use crate::entities::CheckboxGroupConfigurationPB; +use crate::services::field::DateTypeOptionPB; +use crate::services::group::GroupController; + +// pub type CheckboxGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs new file mode 100644 index 0000000000..9bb2c2334d --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -0,0 +1,88 @@ +use crate::services::cell::{decode_any_cell_data, CellBytes}; +use bytes::Bytes; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::{ + CellRevision, FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer, +}; +use std::marker::PhantomData; +use std::sync::Arc; + +pub trait GroupAction { + fn should_group(&mut self, content: &str, cell_bytes: CellBytes) -> bool; +} + +pub trait GroupCellContentProvider { + /// We need to group the rows base on the deduplication cell content when the field type is + /// RichText. + fn deduplication_cell_content(&self, field_id: &str) -> Vec { + vec![] + } +} + +pub trait GroupGenerator { + fn gen_groups( + configuration: &Option, + type_option: &Option, + cell_content_provider: &dyn GroupCellContentProvider, + ) -> Vec; +} + +pub struct GroupController { + field_rev: Arc, + groups: Vec, + type_option: Option, + configuration: Option, + phantom: PhantomData, +} + +pub struct Group { + row_ids: Vec, + content: String, +} + +impl GroupController +where + C: TryFrom, + T: TypeOptionDataDeserializer, + G: GroupGenerator, +{ + pub fn new( + field_rev: Arc, + configuration: GroupConfigurationRevision, + cell_content_provider: &dyn GroupCellContentProvider, + ) -> FlowyResult { + let configuration = match configuration.content { + None => None, + Some(content) => Some(C::try_from(Bytes::from(content))?), + }; + let field_type_rev = field_rev.field_type_rev.clone(); + let type_option = field_rev.get_type_option_entry::(field_type_rev); + Ok(Self { + field_rev, + groups: G::gen_groups(&configuration, &type_option, cell_content_provider), + type_option, + configuration, + phantom: PhantomData, + }) + } +} + +impl GroupController +where + Self: GroupAction, +{ + pub fn group_row(&mut self, row: &RowRevision) { + if self.configuration.is_none() { + return; + } + if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { + for group in self.groups.iter_mut() { + let cell_rev: CellRevision = cell_rev.clone(); + let cell_bytes = decode_any_cell_data(cell_rev.data, &self.field_rev); + // if self.should_group(&group.content, cell_bytes) { + // group.row_ids.push(row.id.clone()); + // } + } + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs similarity index 75% rename from frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs rename to frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs index 37babdb470..dc6fe6eb01 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/impls/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs @@ -1,11 +1,15 @@ mod checkbox_group; mod date_group; +mod generator; mod number_group; mod select_option_group; mod text_group; +mod url_group; pub use checkbox_group::*; pub use date_group::*; +pub use generator::*; pub use number_group::*; pub use select_option_group::*; pub use text_group::*; +pub use url_group::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs new file mode 100644 index 0000000000..1f0af9f024 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs @@ -0,0 +1,5 @@ +use crate::entities::NumberGroupConfigurationPB; +use crate::services::field::NumberTypeOptionPB; +use crate::services::group::GroupController; + +// pub type NumberGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs new file mode 100644 index 0000000000..bfef713ec5 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -0,0 +1,44 @@ +use crate::entities::SelectOptionGroupConfigurationPB; +use crate::services::cell::CellBytes; +use crate::services::field::{MultiSelectTypeOptionPB, SelectedSelectOptions, SingleSelectTypeOptionPB}; +use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator}; + +pub type SingleSelectGroupController = + GroupController; + +pub struct SingleSelectGroupGen(); +impl GroupGenerator for SingleSelectGroupGen { + fn gen_groups( + configuration: &Option, + type_option: &Option, + cell_content_provider: &dyn GroupCellContentProvider, + ) -> Vec { + todo!() + } +} + +impl GroupAction for SingleSelectGroupController { + fn should_group(&mut self, content: &str, cell_bytes: CellBytes) -> bool { + todo!() + } +} + +pub type MultiSelectGroupController = + GroupController; + +pub struct MultiSelectGroupGen(); +impl GroupGenerator for MultiSelectGroupGen { + fn gen_groups( + configuration: &Option, + type_option: &Option, + cell_content_provider: &dyn GroupCellContentProvider, + ) -> Vec { + todo!() + } +} + +impl GroupAction for MultiSelectGroupController { + fn should_group(&mut self, content: &str, cell_bytes: CellBytes) -> bool { + todo!() + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs new file mode 100644 index 0000000000..4a1a30f0fa --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs @@ -0,0 +1,5 @@ +use crate::entities::TextGroupConfigurationPB; +use crate::services::field::RichTextTypeOptionPB; +use crate::services::group::GroupController; + +// pub type TextGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs new file mode 100644 index 0000000000..edc10913a7 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs @@ -0,0 +1,5 @@ +use crate::entities::UrlGroupConfigurationPB; +use crate::services::field::URLTypeOptionPB; +use crate::services::group::GroupController; + +// pub type UrlGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 5bafd5b841..c3727781fa 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -4,7 +4,9 @@ use crate::entities::{ }; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{decode_any_cell_data, CellBytes}; +use crate::services::field::TextCellDataParser; use crate::services::grid_editor_task::GridServiceTaskScheduler; +use crate::services::group::{GroupAction, GroupCellContentProvider, SingleSelectGroupController}; use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, GroupConfigurationRevision, RowRevision}; @@ -56,94 +58,42 @@ impl GridGroupService { .flatten() .collect::>>(); + // let a = SingleSelectGroupController::new; + // let b = a(field_rev.clone(), configuration, &self.grid_pad); + let groups = match field_type { FieldType::RichText => { - let generator = GroupGenerator::::from_configuration(configuration); + // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::Number => { - let generator = GroupGenerator::::from_configuration(configuration); + // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::DateTime => { - let generator = GroupGenerator::::from_configuration(configuration); + // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::SingleSelect => { - let generator = GroupGenerator::::from_configuration(configuration); + let group_controller = + SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad); } FieldType::MultiSelect => { - let generator = GroupGenerator::::from_configuration(configuration); + // let group_generator = MultiSelectGroupControllern(configuration); } FieldType::Checkbox => { - let generator = GroupGenerator::::from_configuration(configuration); + // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::URL => { - let generator = GroupGenerator::::from_configuration(configuration); + // let generator = GroupGenerator::::from_configuration(configuration); } }; None } } -pub struct GroupGenerator { - field_id: String, - groups: Vec, - configuration: Option, -} - -pub struct Group { - row_ids: Vec, - content: String, -} - -impl GroupGenerator -where - T: TryFrom, -{ - pub fn from_configuration(configuration: GroupConfigurationRevision) -> FlowyResult { - let bytes = Bytes::from(configuration.content.unwrap_or(vec![])); - Self::from_bytes(&configuration.field_id, bytes) - } - - pub fn from_bytes(field_id: &str, bytes: Bytes) -> FlowyResult { - let configuration = if bytes.is_empty() { - None - } else { - Some(T::try_from(bytes)?) - }; - Ok(Self { - field_id: field_id.to_owned(), - groups: vec![], - configuration, - }) - } -} -pub trait GroupConfiguration { - fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool; -} - -impl GroupGenerator -where - T: GroupConfiguration, -{ - pub fn group_row(&mut self, field_rev: &Arc, row: &RowRevision) { - if self.configuration.is_none() { - return; - } - let configuration = self.configuration.as_ref().unwrap(); - if let Some(cell_rev) = row.cells.get(&self.field_id) { - for group in self.groups.iter_mut() { - let cell_rev: CellRevision = cell_rev.clone(); - let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev); - if configuration.should_group(&group.content, cell_bytes) { - group.row_ids.push(row.id.clone()); - } - } - } - } -} - fn find_group_field(field_revs: &[Arc]) -> Option<&Arc> { field_revs.iter().find(|field_rev| { let field_type: FieldType = field_rev.field_type_rev.into(); field_type.can_be_group() }) } + +impl GroupCellContentProvider for Arc> {} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs deleted file mode 100644 index 19a79428c3..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/impls/checkbox_group.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::entities::CheckboxGroupConfigurationPB; -use crate::services::cell::{AnyCellData, CellData, CellGroupOperation}; -use crate::services::field::{CheckboxCellData, CheckboxTypeOption}; -use flowy_error::FlowyResult; - -impl CellGroupOperation for CheckboxTypeOption { - fn apply_group(&self, any_cell_data: AnyCellData, content: &str) -> FlowyResult { - if !any_cell_data.is_checkbox() { - return Ok(true); - } - let cell_data: CellData = any_cell_data.into(); - let checkbox_cell_data = cell_data.try_into_inner()?; - - // Ok(checkbox_cell_data.as_ref() == content) - todo!() - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/date_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/date_group.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/number_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/number_group.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs deleted file mode 100644 index b9e8f5b53b..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/impls/select_option_group.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::entities::SelectOptionGroupConfigurationPB; -use crate::services::field::SelectedSelectOptions; - -impl SelectOptionGroupConfigurationPB { - pub fn is_visible(&self, selected_options: &SelectedSelectOptions) -> bool { - return true; - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/text_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/text_group.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/impls/url_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/impls/url_group.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index f30f63b452..4481e344e8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,5 +1,7 @@ +mod group_configuration; +mod group_generator; mod group_service; -mod impls; +pub(crate) use group_configuration::*; +pub(crate) use group_generator::*; pub(crate) use group_service::*; -pub(crate) use impls::*; From e29aaf83882a715a1911cae7cc24c6ff5506efed Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 10:41:46 +0800 Subject: [PATCH 088/224] refactor: cell data parser --- .../src/services/cell/any_cell_data.rs | 4 +- .../group/group_generator/generator.rs | 42 ++++++++----- .../group_generator/select_option_group.rs | 62 +++++++++++-------- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs index 8ebffcbedc..690f6f225f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs @@ -132,11 +132,11 @@ impl CellBytes { Ok(Self(bytes)) } - pub fn with_parser

(&self, parser: P) -> FlowyResult + pub fn with_parser

(&self) -> FlowyResult where P: CellBytesParser, { - parser.parse(&self.0) + P::parse(&self.0) } // pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index 9bb2c2334d..66e195c814 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -1,14 +1,15 @@ -use crate::services::cell::{decode_any_cell_data, CellBytes}; +use crate::services::cell::{decode_any_cell_data, CellBytes, CellBytesParser}; use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ CellRevision, FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer, }; +use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; -pub trait GroupAction { - fn should_group(&mut self, content: &str, cell_bytes: CellBytes) -> bool; +pub trait GroupAction { + fn should_group(&self, content: &str, cell_data: CD) -> bool; } pub trait GroupCellContentProvider { @@ -24,15 +25,16 @@ pub trait GroupGenerator { configuration: &Option, type_option: &Option, cell_content_provider: &dyn GroupCellContentProvider, - ) -> Vec; + ) -> HashMap; } -pub struct GroupController { +pub struct GroupController { field_rev: Arc, - groups: Vec, + groups: HashMap, type_option: Option, configuration: Option, - phantom: PhantomData, + group_action_phantom: PhantomData, + cell_parser_phantom: PhantomData, } pub struct Group { @@ -40,7 +42,7 @@ pub struct Group { content: String, } -impl GroupController +impl GroupController where C: TryFrom, T: TypeOptionDataDeserializer, @@ -62,27 +64,39 @@ where groups: G::gen_groups(&configuration, &type_option, cell_content_provider), type_option, configuration, - phantom: PhantomData, + group_action_phantom: PhantomData, + cell_parser_phantom: PhantomData, }) } } -impl GroupController +impl GroupController where - Self: GroupAction, + CP: CellBytesParser, + Self: GroupAction, { pub fn group_row(&mut self, row: &RowRevision) { if self.configuration.is_none() { return; } if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { - for group in self.groups.iter_mut() { + let mut group_row_id = None; + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &self.field_rev); + // let cell_data = cell_bytes.with_parser(CP); + for group in self.groups.values() { let cell_rev: CellRevision = cell_rev.clone(); - let cell_bytes = decode_any_cell_data(cell_rev.data, &self.field_rev); + // if self.should_group(&group.content, cell_bytes) { - // group.row_ids.push(row.id.clone()); + // group_row_id = Some(row.id.clone()); + // break; // } } + + if let Some(group_row_id) = group_row_id { + self.groups.get_mut(&group_row_id).map(|group| { + group.row_ids.push(group_row_id); + }); + } } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index bfef713ec5..9de4bf08e3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -1,44 +1,52 @@ use crate::entities::SelectOptionGroupConfigurationPB; use crate::services::cell::CellBytes; -use crate::services::field::{MultiSelectTypeOptionPB, SelectedSelectOptions, SingleSelectTypeOptionPB}; +use crate::services::field::{ + MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SelectedSelectOptions, + SingleSelectTypeOptionPB, +}; use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator}; +use std::collections::HashMap; -pub type SingleSelectGroupController = - GroupController; +pub type SingleSelectGroupController = GroupController< + SelectOptionGroupConfigurationPB, + SingleSelectTypeOptionPB, + SingleSelectGroupGenerator, + SelectOptionCellDataParser, +>; -pub struct SingleSelectGroupGen(); -impl GroupGenerator for SingleSelectGroupGen { +pub struct SingleSelectGroupGenerator(); +impl GroupGenerator for SingleSelectGroupGenerator { fn gen_groups( configuration: &Option, type_option: &Option, cell_content_provider: &dyn GroupCellContentProvider, - ) -> Vec { + ) -> HashMap { todo!() } } -impl GroupAction for SingleSelectGroupController { - fn should_group(&mut self, content: &str, cell_bytes: CellBytes) -> bool { +impl GroupAction for SingleSelectGroupController { + fn should_group(&self, content: &str, cell_data: SelectOptionCellDataPB) -> bool { todo!() } } -pub type MultiSelectGroupController = - GroupController; - -pub struct MultiSelectGroupGen(); -impl GroupGenerator for MultiSelectGroupGen { - fn gen_groups( - configuration: &Option, - type_option: &Option, - cell_content_provider: &dyn GroupCellContentProvider, - ) -> Vec { - todo!() - } -} - -impl GroupAction for MultiSelectGroupController { - fn should_group(&mut self, content: &str, cell_bytes: CellBytes) -> bool { - todo!() - } -} +// pub type MultiSelectGroupController = +// GroupController; +// +// pub struct MultiSelectGroupGenerator(); +// impl GroupGenerator for MultiSelectGroupGenerator { +// fn gen_groups( +// configuration: &Option, +// type_option: &Option, +// cell_content_provider: &dyn GroupCellContentProvider, +// ) -> HashMap { +// todo!() +// } +// } +// +// impl GroupAction for MultiSelectGroupController { +// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { +// todo!() +// } +// } From 1b5d717560494eec57a10b0bd6d984cd29bc3397 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 12 Aug 2022 13:40:35 +0800 Subject: [PATCH 089/224] fix: #832 --- .../internal_key_event_handlers/delete_text_handler.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 5e96e75a99..92f5e9afb9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -4,12 +4,13 @@ import 'package:flutter/services.dart'; import 'package:flowy_editor/flowy_editor.dart'; KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { - final selection = editorState.service.selectionService.currentSelection.value; + var selection = editorState.service.selectionService.currentSelection.value; if (selection == null) { return KeyEventResult.ignored; } - - final nodes = editorState.service.selectionService.currentSelectedNodes; + var nodes = editorState.service.selectionService.currentSelectedNodes; + nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); + selection = selection.isBackward ? selection : selection.reversed; // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); if (textNodes.length != nodes.length) { @@ -20,7 +21,7 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { if (textNodes.length == 1) { final textNode = textNodes.first; final index = textNode.delta.prevRunePosition(selection.start.offset); - if (index < 0) { + if (index < 0 && selection.isCollapsed) { // 1. style if (textNode.subtype != null) { transactionBuilder From f2c624778e6050b396a2be8e04e61be591ed2d6e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 13:20:07 +0800 Subject: [PATCH 090/224] feat(doc): transaction and deltas --- .../lib/src/document/text_delta.dart | 36 ++++++++++++++++++- .../src/operation/transaction_builder.dart | 19 +++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 6a4e4f3c80..05f0d61b8d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -256,7 +256,12 @@ TextOperation? _textOperationFromJson(Map json) { return result; } -// basically copy from: https://github.com/quilljs/delta +/// Deltas are a simple, yet expressive format that can be used to describe contents and changes. +/// The format is JSON based, and is human readable, yet easily parsible by machines. +/// Deltas can describe any rich text document, includes all text and formatting information, without the ambiguity and complexity of HTML. +/// + +/// Basically borrowed from: https://github.com/quilljs/delta class Delta extends Iterable { final List _operations; String? _rawString; @@ -316,6 +321,9 @@ class Delta extends Iterable { _operations.add(textOp); } + /// The slice() method does not change the original string. + /// The start and end parameters specifies the part of the string to extract. + /// The end position is optional. Delta slice(int start, [int? end]) { final result = Delta(); final iterator = _OpIterator(_operations); @@ -336,19 +344,29 @@ class Delta extends Iterable { return result; } + /// Insert operations have an `insert` key defined. + /// A String value represents inserting text. void insert(String content, [Attributes? attributes]) => add(TextInsert(content, attributes)); + /// Retain operations have a Number `retain` key defined representing the number of characters to keep (other libraries might use the name keep or skip). + /// An optional `attributes` key can be defined with an Object to describe formatting changes to the character range. + /// A value of `null` in the `attributes` Object represents removal of that key. + /// + /// *Note: It is not necessary to retain the last characters of a document as this is implied.* void retain(int length, [Attributes? attributes]) => add(TextRetain(length, attributes)); + /// Delete operations have a Number `delete` key defined representing the number of characters to delete. void delete(int length) => add(TextDelete(length)); + /// The length of the string fo the [Delta]. int get length { return _operations.fold( 0, (previousValue, element) => previousValue + element.length); } + /// Returns a Delta that is equivalent to applying the operations of own Delta, followed by another Delta. Delta compose(Delta other) { final thisIter = _OpIterator(_operations); final otherIter = _OpIterator(other._operations); @@ -412,6 +430,7 @@ class Delta extends Iterable { return delta..chop(); } + /// This method joins two Delta together. Delta operator +(Delta other) { var ops = [..._operations]; if (other._operations.isNotEmpty) { @@ -445,6 +464,7 @@ class Delta extends Iterable { return hashList(_operations); } + /// Returned an inverted delta that has the opposite effect of against a base document delta. Delta invert(Delta base) { final inverted = Delta(); _operations.fold(0, (int previousValue, op) { @@ -475,6 +495,13 @@ class Delta extends Iterable { return _operations.map((e) => e.toJson()).toList(); } + /// This method will return the position of the previous rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position - 1` simply. + /// + /// This method can help you to compute the position of the previous character. int prevRunePosition(int pos) { if (pos == 0) { return pos - 1; @@ -485,6 +512,13 @@ class Delta extends Iterable { return _runeIndexes![pos - 1]; } + /// This method will return the position of the next rune. + /// + /// Since the encoding of the [String] in Dart is UTF-16. + /// If you want to find the previous character of a position, + /// you can' just use the `position + 1` simply. + /// + /// This method can help you to compute the position of the next character. int nextRunePosition(int pos) { final stringContent = toRawString(); if (pos >= stringContent.length - 1) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index 5a8dc68b3b..f8cc80b161 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -14,7 +14,6 @@ import 'package:flowy_editor/src/operation/transaction.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save make a snapshot of the cursor selection state automatically. /// The cursor can be resorted if the transaction is undo. - class TransactionBuilder { final List operations = []; EditorState state; @@ -29,15 +28,18 @@ class TransactionBuilder { state.apply(transaction); } + /// Insert the nodes at the position of path. insertNode(Path path, Node node) { insertNodes(path, [node]); } + /// Insert a sequence of nodes at the position of path. insertNodes(Path path, List nodes) { beforeSelection = state.cursorSelection; add(InsertOperation(path, nodes)); } + /// Update the attributes of nodes. updateNode(Node node, Attributes attributes) { beforeSelection = state.cursorSelection; @@ -49,6 +51,7 @@ class TransactionBuilder { )); } + /// Delete a node in the document. deleteNode(Node node) { deleteNodesAtPath(node.path); } @@ -57,6 +60,9 @@ class TransactionBuilder { nodes.forEach(deleteNode); } + /// Delete a sequence of nodes at the path of the document. + /// The length specific the length of the following nodes to delete( + /// including the start one). deleteNodesAtPath(Path path, [int length = 1]) { if (path.isEmpty) { return; @@ -106,6 +112,9 @@ class TransactionBuilder { ); } + /// Insert content at a specified index. + /// Optionally, you may specify formatting attributes that are applied to the inserted string. + /// By default, the formatting attributes before the insert position will be used. insertText(TextNode node, int index, String content, [Attributes? attributes]) { var newAttributes = attributes; @@ -126,6 +135,7 @@ class TransactionBuilder { Position(path: node.path, offset: index + content.length)); } + /// Assign formatting attributes to a range of text. formatText(TextNode node, int index, int length, Attributes attributes) { textEdit( node, @@ -135,6 +145,7 @@ class TransactionBuilder { afterSelection = beforeSelection; } + /// Delete length characters starting from index. deleteText(TextNode node, int index, int length) { textEdit( node, @@ -169,6 +180,11 @@ class TransactionBuilder { ); } + /// Add an operation to the transaction. + /// This method will merge operations if they are both TextEdits. + /// + /// Also, this method will transform the path of the operations + /// to avoid conflicts. add(Operation op) { final Operation? last = operations.isEmpty ? null : operations.last; if (last != null) { @@ -190,6 +206,7 @@ class TransactionBuilder { operations.add(op); } + /// Generate a immutable [Transaction] to apply or transmit. Transaction finish() { return Transaction( operations: UnmodifiableListView(operations), From f0ed15440a887390fe18cc1e7d68cc3a03ee4cbd Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 14:00:56 +0800 Subject: [PATCH 091/224] feat(doc): document's state --- .../packages/flowy_editor/lib/src/editor_state.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 31a321a3c0..4b387afd9e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -26,6 +26,15 @@ enum CursorUpdateReason { others, } +/// The state of the editor. +/// +/// The state including: +/// - The document to render +/// - The state of the selection. +/// +/// [EditorState] also includes the services of the editor: +/// - Selection service +/// - Scroll service class EditorState { final StateTree document; From 76a317a9fb6797f2cbc48717e093350262d72be5 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 14:03:35 +0800 Subject: [PATCH 092/224] refactor: remove delted nodes --- .../flowy_editor/lib/src/editor_state.dart | 2 -- .../default_key_event_handlers.dart | 2 -- .../delete_nodes_handler.dart | 21 ------------------- 3 files changed, 25 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 4b387afd9e..bc55860c24 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -38,8 +38,6 @@ enum CursorUpdateReason { class EditorState { final StateTree document; - List selectedNodes = []; - // Service reference. final service = FlowyService(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index e617804a77..a327206c26 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -1,6 +1,5 @@ import 'package:flowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_nodes_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; import 'package:flowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; @@ -14,7 +13,6 @@ import 'package:flowy_editor/src/service/keyboard_service.dart'; List defaultKeyEventHandlers = [ deleteTextHandler, slashShortcutHandler, - flowyDeleteNodesHandler, arrowKeysHandler, copyPasteKeysHandler, redoUndoKeysHandler, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart deleted file mode 100644 index 132f88854a..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_nodes_handler.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/src/service/keyboard_service.dart'; -import 'package:flutter/material.dart'; - -FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { - // Handle delete nodes. - final nodes = editorState.selectedNodes; - if (nodes.length <= 1) { - return KeyEventResult.ignored; - } - - debugPrint('delete nodes = $nodes'); - - nodes - .fold( - TransactionBuilder(editorState), - (previousValue, node) => previousValue..deleteNode(node), - ) - .commit(); - return KeyEventResult.handled; -}; From 7d6f872fed7ae2c941d6023693d5e3867791251b Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 14:07:21 +0800 Subject: [PATCH 093/224] feat(doc): EditorState --- .../flowy_editor/lib/src/editor_state.dart | 15 +++++++++++++-- .../flowy_editor/lib/src/undo_manager.dart | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index bc55860c24..6b7210cd7a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; -import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/document/selection.dart'; import 'package:flowy_editor/src/document/state_tree.dart'; import 'package:flowy_editor/src/operation/operation.dart'; @@ -35,6 +34,14 @@ enum CursorUpdateReason { /// [EditorState] also includes the services of the editor: /// - Selection service /// - Scroll service +/// - Keyboard service +/// - Input service +/// - Toolbar service +/// +/// In consideration of collaborative editing. +/// All the mutations should be applied through [Transaction]. +/// +/// Mutating the document with document's API is not recommended. class EditorState { final StateTree document; @@ -48,7 +55,6 @@ class EditorState { return _cursorSelection; } - /// add the set reason in the future, don't use setter updateCursorSelection(Selection? cursorSelection, [CursorUpdateReason reason = CursorUpdateReason.others]) { // broadcast to other users here @@ -66,8 +72,13 @@ class EditorState { undoManager.state = this; } + /// Apply the transaction to the state. + /// + /// The options can be used to determine whether the editor + /// should record the transaction in undo/redo stack. apply(Transaction transaction, [ApplyOptions options = const ApplyOptions()]) { + // TODO: validate the transation. for (final op in transaction.operations) { _applyOperation(op); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart index b81e91481f..e49debdd09 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/undo_manager.dart @@ -18,6 +18,11 @@ class HistoryItem extends LinkedListEntry { HistoryItem(); + /// Seal the history item. + /// When an item is sealed, no more operations can be added + /// to the item. + /// + /// The caller should create a new [HistoryItem]. seal() { _sealed = true; } @@ -32,6 +37,7 @@ class HistoryItem extends LinkedListEntry { operations.addAll(iterable); } + /// Create a new [Transaction] by inverting the operations. Transaction toTransaction(EditorState state) { final builder = TransactionBuilder(state); for (var i = operations.length - 1; i >= 0; i--) { From ee80fd5d972b4ed4e5c57824b4747c7250f1d85e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 15:26:34 +0800 Subject: [PATCH 094/224] feat: copy underline --- .../flowy_editor/lib/src/document/selection.dart | 7 +++++++ .../flowy_editor/lib/src/infra/html_converter.dart | 11 ++++++++++- .../copy_paste_handler.dart | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart index 641a985577..3ac7293a2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/selection.dart @@ -46,6 +46,13 @@ class Selection { (start.path <= end.path && !pathEquals(start.path, end.path)) || (isSingle && start.offset < end.offset); + Selection normalize() { + if (isForward) { + return Selection(start: end, end: start); + } + return this; + } + Selection get reversed => copyWith(start: end, end: start); Selection collapse({bool atStart = false}) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index e16237c0fa..2ec4bd382e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -18,6 +18,7 @@ const String tagParagraph = "p"; const String tagImage = "img"; const String tagAnchor = "a"; const String tagBold = "b"; +const String tagUnderline = "u"; const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; @@ -54,7 +55,8 @@ class HTMLToNodesConverter { if (child.localName == tagAnchor || child.localName == tagSpan || child.localName == tagCode || - child.localName == tagStrong) { + child.localName == tagStrong || + child.localName == tagUnderline) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { // Google docs wraps the the content inside the `` tag. @@ -203,6 +205,8 @@ class HTMLToNodesConverter { delta.insert(element.text, attributes); } else if (element.localName == tagStrong || element.localName == tagBold) { delta.insert(element.text, {"bold": true}); + } else if (element.localName == tagUnderline) { + delta.insert(element.text, {"underline": true}); } else { delta.insert(element.text); } @@ -454,6 +458,11 @@ class NodesToHTMLConverter { final strong = html.Element.tag(tagStrong); strong.append(html.Text(op.content)); childNodes.add(strong); + } else if (attributes.length == 1 && + attributes[StyleKey.underline] == true) { + final strong = html.Element.tag(tagUnderline); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { final span = html.Element.tag(tagSpan); final cssString = _attributesToCssStyle(attributes); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index a5f392a4eb..363b84967a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -6,10 +6,11 @@ import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; _handleCopy(EditorState editorState) async { - final selection = editorState.cursorSelection; + var selection = editorState.cursorSelection; if (selection == null || selection.isCollapsed) { return; } + selection = selection.normalize(); if (pathEquals(selection.start.path, selection.end.path)) { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { From 690ec5cc94c08070414090f606426d10f1627b64 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 15:29:12 +0800 Subject: [PATCH 095/224] feat: italic --- .../flowy_editor/lib/src/infra/html_converter.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 2ec4bd382e..f5f37f4c4e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -17,6 +17,7 @@ const String tagList = "li"; const String tagParagraph = "p"; const String tagImage = "img"; const String tagAnchor = "a"; +const String tagItalic = "i"; const String tagBold = "b"; const String tagUnderline = "u"; const String tagStrong = "strong"; @@ -56,7 +57,8 @@ class HTMLToNodesConverter { child.localName == tagSpan || child.localName == tagCode || child.localName == tagStrong || - child.localName == tagUnderline) { + child.localName == tagUnderline || + child.localName == tagItalic) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { // Google docs wraps the the content inside the `` tag. @@ -207,6 +209,8 @@ class HTMLToNodesConverter { delta.insert(element.text, {"bold": true}); } else if (element.localName == tagUnderline) { delta.insert(element.text, {"underline": true}); + } else if (element.localName == tagItalic) { + delta.insert(element.text, {"italic": true}); } else { delta.insert(element.text); } @@ -463,6 +467,11 @@ class NodesToHTMLConverter { final strong = html.Element.tag(tagUnderline); strong.append(html.Text(op.content)); childNodes.add(strong); + } else if (attributes.length == 1 && + attributes[StyleKey.italic] == true) { + final strong = html.Element.tag(tagItalic); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { final span = html.Element.tag(tagSpan); final cssString = _attributesToCssStyle(attributes); From 5de6a7447a13876596b6bc4ae60597cfd46c82e3 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 16:05:56 +0800 Subject: [PATCH 096/224] chore: read group data from backend --- .../entities/group_entities/checkbox_group.rs | 7 -- .../entities/group_entities/configuration.rs | 56 +++++++++ .../src/entities/group_entities/date_group.rs | 26 ----- .../src/entities/group_entities/mod.rs | 14 +-- .../entities/group_entities/number_group.rs | 7 -- .../group_entities/select_option_group.rs | 7 -- .../src/entities/group_entities/text_group.rs | 7 -- .../src/entities/group_entities/url_group.rs | 7 -- .../src/services/cell/any_cell_data.rs | 16 ++- .../checkbox_type_option_entities.rs | 2 +- .../date_type_option/date_tests.rs | 4 +- .../date_type_option_entities.rs | 2 +- .../number_type_option_entities.rs | 15 ++- .../multi_select_type_option.rs | 2 +- .../selection_type_option/select_option.rs | 4 +- .../single_select_type_option.rs | 2 +- .../text_type_option/text_type_option.rs | 8 +- .../type_options/url_type_option/url_tests.rs | 2 +- .../url_type_option_entities.rs | 2 +- .../flowy-grid/src/services/grid_editor.rs | 3 +- .../src/services/group/group_configuration.rs | 70 ----------- .../group/group_generator/checkbox_group.rs | 46 +++++++- .../group/group_generator/date_group.rs | 5 - .../group/group_generator/generator.rs | 109 ++++++++++++------ .../src/services/group/group_generator/mod.rs | 8 -- .../group/group_generator/number_group.rs | 5 - .../group_generator/select_option_group.rs | 99 +++++++++++----- .../group/group_generator/text_group.rs | 5 - .../group/group_generator/url_group.rs | 5 - .../src/services/group/group_service.rs | 100 ++++++++++++---- .../flowy-grid/src/services/group/mod.rs | 2 - .../tests/grid/block_test/script.rs | 14 +-- .../rust-lib/flowy-grid/tests/grid/script.rs | 2 +- .../src/client_grid/grid_revision_pad.rs | 2 +- 34 files changed, 365 insertions(+), 300 deletions(-) delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/date_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs deleted file mode 100644 index ba121694a7..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/checkbox_group.rs +++ /dev/null @@ -1,7 +0,0 @@ -use flowy_derive::ProtoBuf; - -#[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-grid/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs new file mode 100644 index 0000000000..baa39d91a2 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/configuration.rs @@ -0,0 +1,56 @@ +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 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-grid/src/entities/group_entities/date_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/date_group.rs deleted file mode 100644 index c117b22f8b..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/date_group.rs +++ /dev/null @@ -1,26 +0,0 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; - -#[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 - } -} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs index 1ededd3188..c1834c1009 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs @@ -1,15 +1,5 @@ -mod checkbox_group; -mod date_group; +mod configuration; mod group; -mod number_group; -mod select_option_group; -mod text_group; -mod url_group; -pub use checkbox_group::*; -pub use date_group::*; +pub use configuration::*; pub use group::*; -pub use number_group::*; -pub use select_option_group::*; -pub use text_group::*; -pub use url_group::*; diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs deleted file mode 100644 index ddb63a6ce5..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/number_group.rs +++ /dev/null @@ -1,7 +0,0 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct NumberGroupConfigurationPB { - #[pb(index = 1)] - hide_empty: bool, -} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs deleted file mode 100644 index 67dfe8d1b7..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/select_option_group.rs +++ /dev/null @@ -1,7 +0,0 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct SelectOptionGroupConfigurationPB { - #[pb(index = 1)] - hide_empty: bool, -} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs deleted file mode 100644 index b349850843..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/text_group.rs +++ /dev/null @@ -1,7 +0,0 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct TextGroupConfigurationPB { - #[pb(index = 1)] - hide_empty: bool, -} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs deleted file mode 100644 index 955cb60ae3..0000000000 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/url_group.rs +++ /dev/null @@ -1,7 +0,0 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct UrlGroupConfigurationPB { - #[pb(index = 1)] - hide_empty: bool, -} diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs index 690f6f225f..8f76ea62c5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs @@ -114,6 +114,11 @@ impl AnyCellData { pub struct CellBytes(pub Bytes); pub trait CellBytesParser { + type Object; + fn parser(bytes: &Bytes) -> FlowyResult; +} + +pub trait CellBytesCustomParser { type Object; fn parse(&self, bytes: &Bytes) -> FlowyResult; } @@ -132,11 +137,18 @@ impl CellBytes { Ok(Self(bytes)) } - pub fn with_parser

(&self) -> FlowyResult + pub fn parser

(&self) -> FlowyResult where P: CellBytesParser, { - P::parse(&self.0) + 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 diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs index 3da9ebd23f..cfc123fad9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -60,7 +60,7 @@ impl ToString for CheckboxCellData { pub struct CheckboxCellDataParser(); impl CellBytesParser for CheckboxCellDataParser { type Object = CheckboxCellData; - fn parse(&self, bytes: &Bytes) -> FlowyResult { + fn parser(bytes: &Bytes) -> FlowyResult { match String::from_utf8(bytes.to_vec()) { Ok(s) => CheckboxCellData::from_str(&s), Err(_) => Ok(CheckboxCellData("".to_string())), diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs index 5bdc39ed71..ea5a33871a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs @@ -3,7 +3,7 @@ mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; use crate::services::field::*; - // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat}; + // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOptionPB, TimeFormat}; use flowy_grid_data_model::revision::FieldRevision; use strum::IntoEnumIterator; @@ -137,7 +137,7 @@ mod tests { let decoded_data = type_option .decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev) .unwrap() - .with_parser(DateCellDataParser()) + .parser::() .unwrap(); if type_option.include_time { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs index 1c54606f75..98be107941 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -204,7 +204,7 @@ pub struct DateCellDataParser(); impl CellBytesParser for DateCellDataParser { type Object = DateCellDataPB; - fn parse(&self, bytes: &Bytes) -> FlowyResult { + fn parser(bytes: &Bytes) -> FlowyResult { DateCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs index 6297114a07..aa372222b8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs @@ -1,4 +1,4 @@ -use crate::services::cell::CellBytesParser; +use crate::services::cell::{CellBytesCustomParser, CellBytesParser}; use crate::services::field::number_currency::Currency; use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL}; use bytes::Bytes; @@ -93,8 +93,19 @@ impl ToString for NumberCellData { } } } -pub struct NumberCellDataParser(pub NumberFormat); +pub struct NumberCellDataParser(); impl CellBytesParser for NumberCellDataParser { + type Object = NumberCellData; + fn parser(bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => NumberCellData::from_format_str(&s, true, &NumberFormat::Num), + Err(_) => Ok(NumberCellData::default()), + } + } +} + +pub struct NumberCellCustomDataParser(pub NumberFormat); +impl CellBytesCustomParser for NumberCellCustomDataParser { type Object = NumberCellData; fn parse(&self, bytes: &Bytes) -> FlowyResult { match String::from_utf8(bytes.to_vec()) { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index a09c81e97f..0bffef4c52 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -180,7 +180,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &field_type, field_rev) .unwrap() - .with_parser(SelectOptionCellDataParser()) + .parser::() .unwrap() .select_options, ); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 3e6c95c5ec..5538ff24c7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -206,7 +206,7 @@ impl std::ops::DerefMut for SelectOptionIds { pub struct SelectOptionIdsParser(); impl CellBytesParser for SelectOptionIdsParser { type Object = SelectOptionIds; - fn parse(&self, bytes: &Bytes) -> FlowyResult { + fn parser(bytes: &Bytes) -> FlowyResult { match String::from_utf8(bytes.to_vec()) { Ok(s) => Ok(SelectOptionIds::from(s)), Err(_) => Ok(SelectOptionIds::from("".to_owned())), @@ -218,7 +218,7 @@ pub struct SelectOptionCellDataParser(); impl CellBytesParser for SelectOptionCellDataParser { type Object = SelectOptionCellDataPB; - fn parse(&self, bytes: &Bytes) -> FlowyResult { + fn parser(bytes: &Bytes) -> FlowyResult { SelectOptionCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs index 8722b3a479..2380079a7f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -162,7 +162,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &field_type, field_rev) .unwrap() - .with_parser(SelectOptionCellDataParser()) + .parser::() .unwrap() .select_options, ); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index 00ea95125f..f21ca91f8c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -96,7 +96,7 @@ impl FromCellString for TextCellData { pub struct TextCellDataParser(); impl CellBytesParser for TextCellDataParser { type Object = TextCellData; - fn parse(&self, bytes: &Bytes) -> FlowyResult { + fn parser(bytes: &Bytes) -> FlowyResult { match String::from_utf8(bytes.to_vec()) { Ok(s) => Ok(TextCellData(s)), Err(_) => Ok(TextCellData("".to_owned())), @@ -124,7 +124,7 @@ mod tests { type_option .decode_cell_data(1647251762.to_string().into(), &field_type, &date_time_field_rev) .unwrap() - .with_parser(DateCellDataParser()) + .parser::() .unwrap() .date, "Mar 14,2022".to_owned() @@ -144,7 +144,7 @@ mod tests { &single_select_field_rev ) .unwrap() - .with_parser(SelectOptionCellDataParser()) + .parser::() .unwrap() .select_options, vec![done_option], @@ -167,7 +167,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &FieldType::MultiSelect, &multi_select_field_rev) .unwrap() - .with_parser(SelectOptionCellDataParser()) + .parser::() .unwrap() .select_options, vec![google_option, facebook_option] diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs index ddccfdb606..97b8275287 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs @@ -184,7 +184,7 @@ mod tests { type_option .decode_cell_data(encoded_data.into(), field_type, field_rev) .unwrap() - .with_parser(URLCellDataParser()) + .parser::() .unwrap() } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs index 6ff77cea9a..46e67fdf18 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -30,7 +30,7 @@ pub struct URLCellDataParser(); impl CellBytesParser for URLCellDataParser { type Object = URLCellDataPB; - fn parse(&self, bytes: &Bytes) -> FlowyResult { + fn parser(bytes: &Bytes) -> FlowyResult { URLCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index b0cab2f5af..f2e28701df 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -564,8 +564,9 @@ impl GridRevisionEditor { }) } + #[tracing::instrument(level = "trace", skip_all, err)] pub async fn load_groups(&self) -> FlowyResult { - let groups = self.group_service.load_groups().await.unwrap_or(vec![]); + let groups = self.group_service.load_groups().await.unwrap_or_default(); Ok(RepeatedGridGroupPB { items: groups }) } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs deleted file mode 100644 index b67e4f5404..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_configuration.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::entities::{ - CheckboxGroupConfigurationPB, DateGroupConfigurationPB, NumberGroupConfigurationPB, - SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, -}; -use crate::services::cell::CellBytes; -use crate::services::field::{ - CheckboxCellDataParser, DateCellDataParser, NumberCellDataParser, NumberFormat, SelectOptionCellDataParser, - TextCellDataParser, URLCellDataParser, -}; -use crate::services::group::GroupAction; - -// impl GroupAction for TextGroupConfigurationPB { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// if let Ok(cell_data) = cell_bytes.with_parser(TextCellDataParser()) { -// cell_data.as_ref() == content -// } else { -// false -// } -// } -// } -// -// impl GroupAction for NumberGroupConfigurationPB { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// if let Ok(cell_data) = cell_bytes.with_parser(NumberCellDataParser(NumberFormat::Num)) { -// false -// } else { -// false -// } -// } -// } -// -// impl GroupAction for DateGroupConfigurationPB { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// if let Ok(cell_data) = cell_bytes.with_parser(DateCellDataParser()) { -// false -// } else { -// false -// } -// } -// } -// -// impl GroupAction for SelectOptionGroupConfigurationPB { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// if let Ok(cell_data) = cell_bytes.with_parser(SelectOptionCellDataParser()) { -// false -// } else { -// false -// } -// } -// } -// -// impl GroupAction for UrlGroupConfigurationPB { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// if let Ok(cell_data) = cell_bytes.with_parser(URLCellDataParser()) { -// false -// } else { -// false -// } -// } -// } -// -// impl GroupAction for CheckboxGroupConfigurationPB { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// if let Ok(cell_data) = cell_bytes.with_parser(CheckboxCellDataParser()) { -// false -// } else { -// false -// } -// } -// } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs index 43df16d196..32b699d3a1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -1,7 +1,43 @@ use crate::entities::CheckboxGroupConfigurationPB; -use crate::services::cell::{AnyCellData, CellData, CellGroupOperation}; -use crate::services::field::{CheckboxCellData, CheckboxTypeOptionPB}; -use crate::services::group::GroupController; -use flowy_error::FlowyResult; -// pub type CheckboxGroupGenerator = GroupGenerator; +use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; +use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator}; + +pub type CheckboxGroupController = + GroupController; + +pub struct CheckboxGroupGenerator(); +impl GroupGenerator for CheckboxGroupGenerator { + type ConfigurationType = CheckboxGroupConfigurationPB; + type TypeOptionType = CheckboxTypeOptionPB; + + fn gen_groups( + _configuration: &Option, + _type_option: &Option, + _cell_content_provider: &dyn GroupCellContentProvider, + ) -> Vec { + let check_group = Group { + id: "true".to_string(), + desc: "".to_string(), + rows: vec![], + content: CHECK.to_string(), + }; + + let uncheck_group = Group { + id: "false".to_string(), + desc: "".to_string(), + rows: vec![], + content: UNCHECK.to_string(), + }; + + vec![check_group, uncheck_group] + } +} + +impl GroupAction for CheckboxGroupController { + type CellDataType = CheckboxCellData; + + fn should_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool { + false + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs deleted file mode 100644 index d0cda990d3..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/date_group.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::entities::CheckboxGroupConfigurationPB; -use crate::services::field::DateTypeOptionPB; -use crate::services::group::GroupController; - -// pub type CheckboxGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index 66e195c814..4740e6e7bb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -1,52 +1,70 @@ -use crate::services::cell::{decode_any_cell_data, CellBytes, CellBytesParser}; +use crate::entities::{GroupPB, RowPB}; +use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ - CellRevision, FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer, + FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer, }; -use std::collections::HashMap; +use indexmap::IndexMap; use std::marker::PhantomData; use std::sync::Arc; -pub trait GroupAction { - fn should_group(&self, content: &str, cell_data: CD) -> bool; +pub trait GroupAction { + type CellDataType; + + fn should_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; } pub trait GroupCellContentProvider { /// We need to group the rows base on the deduplication cell content when the field type is /// RichText. - fn deduplication_cell_content(&self, field_id: &str) -> Vec { + fn deduplication_cell_content(&self, _field_id: &str) -> Vec { vec![] } } -pub trait GroupGenerator { +pub trait GroupGenerator { + type ConfigurationType; + type TypeOptionType; + fn gen_groups( - configuration: &Option, - type_option: &Option, + configuration: &Option, + type_option: &Option, cell_content_provider: &dyn GroupCellContentProvider, - ) -> HashMap; + ) -> Vec; } pub struct GroupController { - field_rev: Arc, - groups: HashMap, - type_option: Option, - configuration: Option, + pub field_rev: Arc, + pub groups: IndexMap, + pub type_option: Option, + pub configuration: Option, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData, } pub struct Group { - row_ids: Vec, - content: String, + pub id: String, + pub desc: String, + pub rows: Vec, + pub content: String, +} + +impl std::convert::From for GroupPB { + fn from(group: Group) -> Self { + Self { + group_id: group.id, + desc: group.desc, + rows: group.rows, + } + } } impl GroupController where C: TryFrom, T: TypeOptionDataDeserializer, - G: GroupGenerator, + G: GroupGenerator, { pub fn new( field_rev: Arc, @@ -57,46 +75,63 @@ where None => None, Some(content) => Some(C::try_from(Bytes::from(content))?), }; - let field_type_rev = field_rev.field_type_rev.clone(); + let field_type_rev = field_rev.field_type_rev; let type_option = field_rev.get_type_option_entry::(field_type_rev); + let groups = G::gen_groups(&configuration, &type_option, cell_content_provider); Ok(Self { field_rev, - groups: G::gen_groups(&configuration, &type_option, cell_content_provider), + groups: groups.into_iter().map(|group| (group.id.clone(), group)).collect(), type_option, configuration, group_action_phantom: PhantomData, cell_parser_phantom: PhantomData, }) } + + pub fn take_groups(self) -> Vec { + self.groups.into_values().collect() + } } impl GroupController where CP: CellBytesParser, - Self: GroupAction, + Self: GroupAction, { - pub fn group_row(&mut self, row: &RowRevision) { + pub fn group_rows(&mut self, rows: &[Arc]) -> FlowyResult<()> { if self.configuration.is_none() { - return; + return Ok(()); } - if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { - let mut group_row_id = None; - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &self.field_rev); - // let cell_data = cell_bytes.with_parser(CP); - for group in self.groups.values() { - let cell_rev: CellRevision = cell_rev.clone(); - // if self.should_group(&group.content, cell_bytes) { - // group_row_id = Some(row.id.clone()); - // break; - // } - } + for row in rows { + if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { + let mut records: Vec = vec![]; - if let Some(group_row_id) = group_row_id { - self.groups.get_mut(&group_row_id).map(|group| { - group.row_ids.push(group_row_id); - }); + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &self.field_rev); + let cell_data = cell_bytes.parser::()?; + for group in self.groups.values() { + if self.should_group(&group.content, &cell_data) { + records.push(GroupRecord { + row: row.into(), + group_id: group.id.clone(), + }); + break; + } + } + + for record in records { + if let Some(group) = self.groups.get_mut(&record.group_id) { + group.rows.push(record.row); + } + } } } + + Ok(()) } } + +struct GroupRecord { + row: RowPB, + group_id: String, +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs index dc6fe6eb01..2225087fb8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/mod.rs @@ -1,15 +1,7 @@ mod checkbox_group; -mod date_group; mod generator; -mod number_group; mod select_option_group; -mod text_group; -mod url_group; pub use checkbox_group::*; -pub use date_group::*; pub use generator::*; -pub use number_group::*; pub use select_option_group::*; -pub use text_group::*; -pub use url_group::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs deleted file mode 100644 index 1f0af9f024..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/number_group.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::entities::NumberGroupConfigurationPB; -use crate::services::field::NumberTypeOptionPB; -use crate::services::group::GroupController; - -// pub type NumberGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index 9de4bf08e3..bc4f1c4709 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -1,12 +1,11 @@ use crate::entities::SelectOptionGroupConfigurationPB; -use crate::services::cell::CellBytes; + use crate::services::field::{ - MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SelectedSelectOptions, - SingleSelectTypeOptionPB, + MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB, }; use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator}; -use std::collections::HashMap; +// SingleSelect pub type SingleSelectGroupController = GroupController< SelectOptionGroupConfigurationPB, SingleSelectTypeOptionPB, @@ -15,38 +14,74 @@ pub type SingleSelectGroupController = GroupController< >; pub struct SingleSelectGroupGenerator(); -impl GroupGenerator for SingleSelectGroupGenerator { +impl GroupGenerator for SingleSelectGroupGenerator { + type ConfigurationType = SelectOptionGroupConfigurationPB; + type TypeOptionType = SingleSelectTypeOptionPB; fn gen_groups( - configuration: &Option, - type_option: &Option, - cell_content_provider: &dyn GroupCellContentProvider, - ) -> HashMap { - todo!() + _configuration: &Option, + type_option: &Option, + _cell_content_provider: &dyn GroupCellContentProvider, + ) -> Vec { + match type_option { + None => vec![], + Some(type_option) => type_option + .options + .iter() + .map(|option| Group { + id: option.id.clone(), + desc: option.name.clone(), + rows: vec![], + content: option.id.clone(), + }) + .collect(), + } } } -impl GroupAction for SingleSelectGroupController { - fn should_group(&self, content: &str, cell_data: SelectOptionCellDataPB) -> bool { - todo!() +impl GroupAction for SingleSelectGroupController { + type CellDataType = SelectOptionCellDataPB; + fn should_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { + cell_data.select_options.iter().any(|option| option.id == content) } } -// pub type MultiSelectGroupController = -// GroupController; -// -// pub struct MultiSelectGroupGenerator(); -// impl GroupGenerator for MultiSelectGroupGenerator { -// fn gen_groups( -// configuration: &Option, -// type_option: &Option, -// cell_content_provider: &dyn GroupCellContentProvider, -// ) -> HashMap { -// todo!() -// } -// } -// -// impl GroupAction for MultiSelectGroupController { -// fn should_group(&self, content: &str, cell_bytes: CellBytes) -> bool { -// todo!() -// } -// } +// MultiSelect +pub type MultiSelectGroupController = GroupController< + SelectOptionGroupConfigurationPB, + MultiSelectTypeOptionPB, + MultiSelectGroupGenerator, + SelectOptionCellDataParser, +>; + +pub struct MultiSelectGroupGenerator(); +impl GroupGenerator for MultiSelectGroupGenerator { + type ConfigurationType = SelectOptionGroupConfigurationPB; + type TypeOptionType = MultiSelectTypeOptionPB; + + fn gen_groups( + _configuration: &Option, + type_option: &Option, + _cell_content_provider: &dyn GroupCellContentProvider, + ) -> Vec { + match type_option { + None => vec![], + Some(type_option) => type_option + .options + .iter() + .map(|option| Group { + id: option.id.clone(), + desc: option.name.clone(), + rows: vec![], + content: option.id.clone(), + }) + .collect(), + } + } +} + +impl GroupAction for MultiSelectGroupController { + type CellDataType = SelectOptionCellDataPB; + fn should_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { + cell_data.select_options.iter().any(|option| option.id == content) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs deleted file mode 100644 index 4a1a30f0fa..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/text_group.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::entities::TextGroupConfigurationPB; -use crate::services::field::RichTextTypeOptionPB; -use crate::services::group::GroupController; - -// pub type TextGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs deleted file mode 100644 index edc10913a7..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/url_group.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::entities::UrlGroupConfigurationPB; -use crate::services::field::URLTypeOptionPB; -use crate::services::group::GroupController; - -// pub type UrlGroupGenerator = GroupGenerator; diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index c3727781fa..5be0141ca9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,15 +1,16 @@ +use crate::services::block_manager::GridBlockManager; +use crate::services::grid_editor_task::GridServiceTaskScheduler; +use crate::services::group::{ + CheckboxGroupController, Group, GroupCellContentProvider, MultiSelectGroupController, SingleSelectGroupController, +}; + use crate::entities::{ CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, NumberGroupConfigurationPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, }; -use crate::services::block_manager::GridBlockManager; -use crate::services::cell::{decode_any_cell_data, CellBytes}; -use crate::services::field::TextCellDataParser; -use crate::services::grid_editor_task::GridServiceTaskScheduler; -use crate::services::group::{GroupAction, GroupCellContentProvider, SingleSelectGroupController}; use bytes::Bytes; use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, GroupConfigurationRevision, RowRevision}; +use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision}; use flowy_sync::client_grid::GridRevisionPad; use std::sync::Arc; use tokio::sync::RwLock; @@ -39,53 +40,102 @@ impl GridGroupService { pub(crate) async fn load_groups(&self) -> Option> { let grid_pad = self.grid_pad.read().await; - let field_rev = find_group_field(grid_pad.fields())?; - let field_type: FieldType = field_rev.field_type_rev.clone().into(); - let setting = grid_pad.get_setting_rev(); - let mut configurations = setting.get_groups(&setting.layout, &field_rev.id, &field_rev.field_type_rev)?; - - if configurations.is_empty() { - return None; - } - assert_eq!(configurations.len(), 1); - let configuration = (&*configurations.pop().unwrap()).clone(); + let field_rev = find_group_field(grid_pad.fields()).unwrap(); + let field_type: FieldType = field_rev.field_type_rev.into(); + let configuration = self.get_group_configuration(field_rev).await; let blocks = self.block_manager.get_block_snapshots(None).await.unwrap(); - let row_revs = blocks .into_iter() .map(|block| block.row_revs) .flatten() .collect::>>(); - // let a = SingleSelectGroupController::new; - // let b = a(field_rev.clone(), configuration, &self.grid_pad); + match self.build_groups(&field_type, field_rev, row_revs, configuration) { + Ok(groups) => Some(groups), + Err(_) => None, + } + } - let groups = match field_type { + async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { + let grid_pad = self.grid_pad.read().await; + let setting = grid_pad.get_setting_rev(); + let layout = &setting.layout; + let configurations = setting.get_groups(layout, &field_rev.id, &field_rev.field_type_rev); + match configurations { + None => self.default_group_configuration(field_rev), + Some(mut configurations) => { + assert_eq!(configurations.len(), 1); + (&*configurations.pop().unwrap()).clone() + } + } + } + + fn default_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { + let field_type: FieldType = field_rev.field_type_rev.clone().into(); + let bytes: Bytes = match field_type { + FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(), + FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(), + FieldType::DateTime => DateGroupConfigurationPB::default().try_into().unwrap(), + FieldType::SingleSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), + FieldType::MultiSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), + FieldType::Checkbox => CheckboxGroupConfigurationPB::default().try_into().unwrap(), + FieldType::URL => UrlGroupConfigurationPB::default().try_into().unwrap(), + }; + GroupConfigurationRevision { + id: gen_grid_group_id(), + field_id: field_rev.id.clone(), + field_type_rev: field_rev.field_type_rev.clone(), + content: Some(bytes.to_vec()), + } + } + + #[tracing::instrument(level = "trace", skip_all, err)] + fn build_groups( + &self, + field_type: &FieldType, + field_rev: &Arc, + row_revs: Vec>, + configuration: GroupConfigurationRevision, + ) -> FlowyResult> { + let groups: Vec = match field_type { FieldType::RichText => { // let generator = GroupGenerator::::from_configuration(configuration); + vec![] } FieldType::Number => { // let generator = GroupGenerator::::from_configuration(configuration); + vec![] } FieldType::DateTime => { // let generator = GroupGenerator::::from_configuration(configuration); + vec![] } FieldType::SingleSelect => { - let group_controller = - SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad); + let mut group_controller = + SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + let _ = group_controller.group_rows(&row_revs)?; + group_controller.take_groups() } FieldType::MultiSelect => { - // let group_generator = MultiSelectGroupControllern(configuration); + let mut group_controller = + MultiSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + let _ = group_controller.group_rows(&row_revs)?; + group_controller.take_groups() } FieldType::Checkbox => { - // let generator = GroupGenerator::::from_configuration(configuration); + let mut group_controller = + CheckboxGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + let _ = group_controller.group_rows(&row_revs)?; + group_controller.take_groups() } FieldType::URL => { // let generator = GroupGenerator::::from_configuration(configuration); + vec![] } }; - None + + Ok(groups.into_iter().map(GroupPB::from).collect()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs index 4481e344e8..15b401f4c0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/mod.rs @@ -1,7 +1,5 @@ -mod group_configuration; mod group_generator; mod group_service; -pub(crate) use group_configuration::*; pub(crate) use group_generator::*; pub(crate) use group_service::*; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index b25bb044fc..f4e32e1406 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -162,7 +162,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(TextCellDataParser()) + .parser::() .unwrap(); assert_eq!(cell_data.as_ref(), &expected); @@ -177,7 +177,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(NumberCellDataParser(number_type_option.format)) + .custom_parser(NumberCellCustomDataParser(number_type_option.format)) .unwrap(); assert_eq!(cell_data.to_string(), expected); } @@ -187,7 +187,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(DateCellDataParser()) + .parser::() .unwrap(); assert_eq!(cell_data.date, expected); @@ -198,7 +198,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(SelectOptionCellDataParser()) + .parser::() .unwrap(); let select_option = cell_data.select_options.first().unwrap(); assert_eq!(select_option.name, expected); @@ -209,7 +209,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(SelectOptionCellDataParser()) + .parser::() .unwrap(); let s = cell_data @@ -228,7 +228,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(CheckboxCellDataParser()) + .parser::() .unwrap(); assert_eq!(cell_data.to_string(), expected); } @@ -238,7 +238,7 @@ impl GridRowTest { .get_cell_bytes(&cell_id) .await .unwrap() - .with_parser(URLCellDataParser()) + .parser::() .unwrap(); assert_eq!(cell_data.content, expected); diff --git a/frontend/rust-lib/flowy-grid/tests/grid/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/script.rs index c1fa47c7a9..c9ea583a14 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/script.rs @@ -259,7 +259,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) { let cloned_field_meta = field_meta.clone(); let type_option_data = field_meta - .get_type_option_entry::(&field_meta.field_type) + .get_type_option_entry::(&field_meta.field_type) .unwrap() .protobuf_bytes() .to_vec(); diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index a762103e57..46b278a1af 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -572,7 +572,7 @@ fn make_group_revision(params: &CreateGridGroupParams) -> GroupConfigurationRevi GroupConfigurationRevision { id: gen_grid_group_id(), field_id: params.field_id.clone(), - field_type_rev: params.field_type_rev.clone(), + field_type_rev: params.field_type_rev, content: params.content.clone(), } } From 0f404e55274cb5ead4fe1c29f102c2022aceae6e Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 15:31:34 +0800 Subject: [PATCH 097/224] feat: strike --- .../lib/src/infra/html_converter.dart | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index f5f37f4c4e..96c001a318 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -20,6 +20,7 @@ const String tagAnchor = "a"; const String tagItalic = "i"; const String tagBold = "b"; const String tagUnderline = "u"; +const String tagDel = "del"; const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; @@ -58,7 +59,8 @@ class HTMLToNodesConverter { child.localName == tagCode || child.localName == tagStrong || child.localName == tagUnderline || - child.localName == tagItalic) { + child.localName == tagItalic || + child.localName == tagDel) { _handleRichTextElement(delta, child); } else if (child.localName == tagBold) { // Google docs wraps the the content inside the `` tag. @@ -132,7 +134,7 @@ class HTMLToNodesConverter { if (tuples.length < 2) { continue; } - result[tuples[0]] = tuples[1]; + result[tuples[0].trim()] = tuples[1].trim(); } return result; @@ -146,12 +148,23 @@ class HTMLToNodesConverter { final fontWeightStr = cssMap["font-weight"]; if (fontWeightStr != null) { - int? weight = int.tryParse(fontWeightStr); - if (weight != null && weight > 500) { - attrs["bold"] = true; + if (fontWeightStr == "bold") { + attrs[StyleKey.bold] = true; + } else { + int? weight = int.tryParse(fontWeightStr); + if (weight != null && weight > 500) { + attrs[StyleKey.bold] = true; + } } } + final textDecorationStr = cssMap["text-decoration"]; + if (textDecorationStr == "line-through") { + attrs[StyleKey.strikethrough] = true; + } else if (textDecorationStr == "underline") { + attrs[StyleKey.underline] = true; + } + final backgroundColorStr = cssMap["background-color"]; final backgroundColor = _tryParseCssColorString(backgroundColorStr); if (backgroundColor != null) { @@ -159,6 +172,10 @@ class HTMLToNodesConverter { '0x${backgroundColor.value.toRadixString(16)}'; } + if (cssMap["font-style"] == "italic") { + attrs[StyleKey.italic] = true; + } + return attrs.isEmpty ? null : attrs; } @@ -206,11 +223,13 @@ class HTMLToNodesConverter { } delta.insert(element.text, attributes); } else if (element.localName == tagStrong || element.localName == tagBold) { - delta.insert(element.text, {"bold": true}); + delta.insert(element.text, {StyleKey.bold: true}); } else if (element.localName == tagUnderline) { - delta.insert(element.text, {"underline": true}); + delta.insert(element.text, {StyleKey.underline: true}); } else if (element.localName == tagItalic) { - delta.insert(element.text, {"italic": true}); + delta.insert(element.text, {StyleKey.italic: true}); + } else if (element.localName == tagDel) { + delta.insert(element.text, {StyleKey.strikethrough: true}); } else { delta.insert(element.text); } @@ -422,6 +441,15 @@ class NodesToHTMLConverter { if (attributes[StyleKey.bold] == true) { cssMap["font-weight"] = "bold"; } + if (attributes[StyleKey.strikethrough] == true) { + cssMap["text-decoration"] = "line-through"; + } + if (attributes[StyleKey.underline] == true) { + cssMap["text-decoration"] = "underline"; + } + if (attributes[StyleKey.italic] == true) { + cssMap["font-style"] = "italic"; + } return _cssMapToCssStyle(cssMap); } @@ -472,6 +500,11 @@ class NodesToHTMLConverter { final strong = html.Element.tag(tagItalic); strong.append(html.Text(op.content)); childNodes.add(strong); + } else if (attributes.length == 1 && + attributes[StyleKey.strikethrough] == true) { + final strong = html.Element.tag(tagDel); + strong.append(html.Text(op.content)); + childNodes.add(strong); } else { final span = html.Element.tag(tagSpan); final cssString = _attributesToCssStyle(attributes); From 61aaa20113a16914593a4c3001098f8015764e86 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 16:25:50 +0800 Subject: [PATCH 098/224] feat: handle text-decoration for text styles --- .../lib/src/infra/html_converter.dart | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 96c001a318..cc7fd949ae 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -159,10 +159,8 @@ class HTMLToNodesConverter { } final textDecorationStr = cssMap["text-decoration"]; - if (textDecorationStr == "line-through") { - attrs[StyleKey.strikethrough] = true; - } else if (textDecorationStr == "underline") { - attrs[StyleKey.underline] = true; + if (textDecorationStr != null) { + _assignTextDecorations(attrs, textDecorationStr); } final backgroundColorStr = cssMap["background-color"]; @@ -179,6 +177,17 @@ class HTMLToNodesConverter { return attrs.isEmpty ? null : attrs; } + _assignTextDecorations(Attributes attrs, String decorationStr) { + final decorations = decorationStr.split(" "); + for (final d in decorations) { + if (d == "line-through") { + attrs[StyleKey.strikethrough] = true; + } else if (d == "underline") { + attrs[StyleKey.underline] = true; + } + } + } + /// Try to parse the `rgba(red, greed, blue, alpha)` /// from the string. Color? _tryParseCssColorString(String? colorString) { @@ -424,6 +433,18 @@ class NodesToHTMLConverter { checked: textNode.attributes["checkbox"] == true); } + String _textDecorationsFromAttributes(Attributes attributes) { + var textDecoration = []; + if (attributes[StyleKey.strikethrough] == true) { + textDecoration.add("line-through"); + } + if (attributes[StyleKey.underline] == true) { + textDecoration.add("underline"); + } + + return textDecoration.join(" "); + } + String _attributesToCssStyle(Map attributes) { final cssMap = {}; if (attributes[StyleKey.backgroundColor] != null) { @@ -441,12 +462,12 @@ class NodesToHTMLConverter { if (attributes[StyleKey.bold] == true) { cssMap["font-weight"] = "bold"; } - if (attributes[StyleKey.strikethrough] == true) { - cssMap["text-decoration"] = "line-through"; - } - if (attributes[StyleKey.underline] == true) { - cssMap["text-decoration"] = "underline"; + + final textDecoration = _textDecorationsFromAttributes(attributes); + if (textDecoration.isNotEmpty) { + cssMap["text-decoration"] = textDecoration; } + if (attributes[StyleKey.italic] == true) { cssMap["font-style"] = "italic"; } From 055868bae353994b96dde09e38fc1bc91f32d454 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 16:06:30 +0800 Subject: [PATCH 099/224] chore: build board card ui --- frontend/.vscode/launch.json | 2 +- .../plugins/board/application/board_bloc.dart | 35 +++++---- .../card/board_select_option_cell_bloc.dart | 76 +++++++++++++++++++ .../card/board_text_cell_bloc.dart | 66 ++++++++++++++++ .../board/presentation/board_page.dart | 7 +- .../card/board_select_option_cell.dart | 51 +++++++++++++ .../presentation/card/board_text_cell.dart | 49 ++++++++++++ .../board/presentation/{ => card}/card.dart | 2 +- .../presentation/card/card_cell_builder.dart | 0 .../application/cell/checkbox_cell_bloc.dart | 18 +++-- .../grid/application/cell/date_cal_bloc.dart | 38 +++++----- .../grid/application/cell/date_cell_bloc.dart | 12 +-- .../application/cell/number_cell_bloc.dart | 21 ++--- .../cell/select_option_cell_bloc.dart | 12 +-- .../grid/application/cell/text_cell_bloc.dart | 17 +++-- .../grid/application/cell/url_cell_bloc.dart | 17 +++-- .../cell/url_cell_editor_bloc.dart | 17 +++-- .../field/type_option/date_bloc.dart | 6 +- .../field/type_option/number_bloc.dart | 6 +- .../type_option/type_option_context.dart | 40 +++++----- .../widgets/cell/date_cell/date_cell.dart | 2 +- .../widgets/cell/date_cell/date_editor.dart | 41 +++++----- .../select_option_cell.dart | 27 +++---- .../widgets/cell/url_cell/cell_editor.dart | 2 +- .../widgets/cell/url_cell/url_cell.dart | 2 +- .../widgets/header/type_option/builder.dart | 10 +-- .../app_flowy/lib/startup/deps_resolver.dart | 10 +-- .../example/lib/multi_board_list_example.dart | 6 +- .../lib/single_board_list_example.dart | 2 +- .../widgets/board_column/board_column.dart | 6 +- .../board_column/board_column_data.dart | 20 ++--- .../lib/src/widgets/board_data.dart | 4 +- .../reorder_phantom/phantom_controller.dart | 4 +- .../rust-lib/flowy-test/src/event_builder.rs | 8 +- 34 files changed, 447 insertions(+), 189 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart rename frontend/app_flowy/lib/plugins/board/presentation/{ => card}/card.dart (83%) create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 036b5ab35c..0efc79b00e 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -44,7 +44,7 @@ "type": "dart", "preLaunchTask": "AF: Clean + Rebuild All", "env": { - "RUST_LOG": "info" + "RUST_LOG": "trace" }, "cwd": "${workspaceRoot}/app_flowy" }, diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index cbfd0500fb..df79e3154c 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -41,8 +41,6 @@ class BoardBloc extends Bloc { ) {}, ); - // boardDataController.addColumns(_buildColumns()); - on( (event, emit) async { await event.when( @@ -87,6 +85,7 @@ class BoardBloc extends Bloc { return AFBoardColumnData( id: group.groupId, desc: group.desc, + items: _buildRows(group.rows), customData: group, ); }).toList(); @@ -99,6 +98,20 @@ class BoardBloc extends Bloc { ); } + List _buildRows(List rows) { + return rows.map((row) { + final rowInfo = RowInfo( + gridId: _dataController.gridId, + blockId: row.blockId, + id: row.id, + fields: _dataController.fieldCache.unmodifiableFields, + height: row.height.toDouble(), + rawRow: row, + ); + return BoardColumnItem(row: rowInfo); + }).toList(); + } + Future _loadGrid(Emitter emit) async { final result = await _dataController.loadData(); result.fold( @@ -172,21 +185,11 @@ class GridFieldEquatable extends Equatable { UnmodifiableListView get value => UnmodifiableListView(_fields); } -class TextItem extends ColumnItem { - final String s; +class BoardColumnItem extends AFColumnItem { + final RowInfo row; - TextItem(this.s); + BoardColumnItem({required this.row}); @override - String get id => s; -} - -class RichTextItem extends ColumnItem { - final String title; - final String subtitle; - - RichTextItem({required this.title, required this.subtitle}); - - @override - String get id => title; + String get id => row.id; } diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart new file mode 100644 index 0000000000..df36033cfa --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_select_option_cell_bloc.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; + +part 'board_select_option_cell_bloc.freezed.dart'; + +class BoardSelectOptionCellBloc + extends Bloc { + final GridSelectOptionCellController cellController; + void Function()? _onCellChangedFn; + + BoardSelectOptionCellBloc({ + required this.cellController, + }) : super(BoardSelectOptionCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveOptions: (List selectedOptions) { + emit(state.copyWith(selectedOptions: selectedOptions)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((selectOptionContext) { + if (!isClosed) { + add(BoardSelectOptionCellEvent.didReceiveOptions( + selectOptionContext?.selectOptions ?? [], + )); + } + }), + ); + } +} + +@freezed +class BoardSelectOptionCellEvent with _$BoardSelectOptionCellEvent { + const factory BoardSelectOptionCellEvent.initial() = _InitialCell; + const factory BoardSelectOptionCellEvent.didReceiveOptions( + List selectedOptions, + ) = _DidReceiveOptions; +} + +@freezed +class BoardSelectOptionCellState with _$BoardSelectOptionCellState { + const factory BoardSelectOptionCellState({ + required List selectedOptions, + }) = _BoardSelectOptionCellState; + + factory BoardSelectOptionCellState.initial( + GridSelectOptionCellController context) { + final data = context.getCellData(); + + return BoardSelectOptionCellState( + selectedOptions: data?.selectOptions ?? [], + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart new file mode 100644 index 0000000000..e11d7b5ac6 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_text_cell_bloc.dart @@ -0,0 +1,66 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_text_cell_bloc.freezed.dart'; + +class BoardTextCellBloc extends Bloc { + final GridCellController cellController; + void Function()? _onCellChangedFn; + BoardTextCellBloc({ + required this.cellController, + }) : super(BoardTextCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (content) { + emit(state.copyWith(content: content)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(BoardTextCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class BoardTextCellEvent with _$BoardTextCellEvent { + const factory BoardTextCellEvent.initial() = _InitialCell; + const factory BoardTextCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class BoardTextCellState with _$BoardTextCellState { + const factory BoardTextCellState({ + required String content, + }) = _BoardTextCellState; + + factory BoardTextCellState.initial(GridCellController context) => + BoardTextCellState( + content: context.getCellData() ?? "", + ); +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 56a2dfa5f5..f8367c1392 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,13 +1,12 @@ // ignore_for_file: unused_field -import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/board_bloc.dart'; -import 'card.dart'; +import 'card/card.dart'; class BoardPage extends StatelessWidget { final ViewPB view; @@ -87,8 +86,8 @@ class BoardContent extends StatelessWidget { ); } - Widget _buildCard(BuildContext context, ColumnItem item) { - final rowInfo = item as RowInfo; + Widget _buildCard(BuildContext context, AFColumnItem item) { + final rowInfo = (item as BoardColumnItem).row; return AppFlowyColumnItemCard( key: ObjectKey(item), child: BoardCard(rowInfo: rowInfo), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart new file mode 100644 index 0000000000..64a7a124c6 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -0,0 +1,51 @@ +import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BoardSelectOptionCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardSelectOptionCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardSelectOptionCellState(); +} + +class _BoardSelectOptionCellState extends State { + late BoardSelectOptionCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridSelectOptionCellController; + _cellBloc = BoardSelectOptionCellBloc(cellController: cellController) + ..add(const BoardSelectOptionCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return SelectOptionWrap( + selectOptions: state.selectedOptions, + cellControllerBuilder: widget.cellControllerBuilder, + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart new file mode 100644 index 0000000000..fd3b89a9ae --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -0,0 +1,49 @@ +import 'package:app_flowy/plugins/board/application/card/board_text_cell_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BoardTextCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + const BoardTextCell({required this.cellControllerBuilder, Key? key}) + : super(key: key); + + @override + State createState() => _BoardTextCellState(); +} + +class _BoardTextCellState extends State { + late BoardTextCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridCellController; + + _cellBloc = BoardTextCellBloc(cellController: cellController) + ..add(const BoardTextCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return SizedBox( + height: 30, + child: FlowyText.medium(state.content), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart similarity index 83% rename from frontend/app_flowy/lib/plugins/board/presentation/card.dart rename to frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 1410828eea..020cc49db3 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -8,6 +8,6 @@ class BoardCard extends StatelessWidget { @override Widget build(BuildContext context) { - return const Text('1234'); + return const SizedBox(height: 20, child: Text('1234')); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart index 041e687c9b..44b94a3ddf 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart @@ -6,13 +6,13 @@ import 'cell_service/cell_service.dart'; part 'checkbox_cell_bloc.freezed.dart'; class CheckboxCellBloc extends Bloc { - final GridCellController cellContext; + final GridCellController cellController; void Function()? _onCellChangedFn; CheckboxCellBloc({ required CellService service, - required this.cellContext, - }) : super(CheckboxCellState.initial(cellContext)) { + required this.cellController, + }) : super(CheckboxCellState.initial(cellController)) { on( (event, emit) async { await event.when( @@ -33,16 +33,17 @@ class CheckboxCellBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening(onCellChanged: ((cellData) { + _onCellChangedFn = + cellController.startListening(onCellChanged: ((cellData) { if (!isClosed) { add(CheckboxCellEvent.didReceiveCellUpdate(cellData)); } @@ -50,7 +51,7 @@ class CheckboxCellBloc extends Bloc { } void _updateCellData() { - cellContext.saveCellData(!state.isSelected ? "Yes" : "No"); + cellController.saveCellData(!state.isSelected ? "Yes" : "No"); } } @@ -58,7 +59,8 @@ class CheckboxCellBloc extends Bloc { class CheckboxCellEvent with _$CheckboxCellEvent { const factory CheckboxCellEvent.initial() = _Initial; const factory CheckboxCellEvent.select() = _Selected; - const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = _DidReceiveCellUpdate; + const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = + _DidReceiveCellUpdate; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart index 85dbbb40be..c0584a084b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart @@ -18,14 +18,14 @@ import 'package:fixnum/fixnum.dart' as $fixnum; part 'date_cal_bloc.freezed.dart'; class DateCalBloc extends Bloc { - final GridDateCellController cellContext; + final GridDateCellController cellController; void Function()? _onCellChangedFn; DateCalBloc({ - required DateTypeOption dateTypeOption, + required DateTypeOptionPB dateTypeOptionPB, required DateCellDataPB? cellData, - required this.cellContext, - }) : super(DateCalState.initial(dateTypeOption, cellData)) { + required this.cellController, + }) : super(DateCalState.initial(dateTypeOptionPB, cellData)) { on( (event, emit) async { await event.when( @@ -102,7 +102,7 @@ class DateCalBloc extends Bloc { } } - cellContext.saveCellData(newCalData, resultCallback: (result) { + cellController.saveCellData(newCalData, resultCallback: (result) { result.fold( () => updateCalData(Some(newCalData), none()), (err) { @@ -120,7 +120,7 @@ class DateCalBloc extends Bloc { String timeFormatPrompt(FlowyError error) { String msg = LocaleKeys.grid_field_invalidTimeFormat.tr() + ". "; - switch (state.dateTypeOption.timeFormat) { + switch (state.dateTypeOptionPB.timeFormat) { case TimeFormat.TwelveHour: msg = msg + "e.g. 01: 00 AM"; break; @@ -136,15 +136,15 @@ class DateCalBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((cell) { if (!isClosed) { add(DateCalEvent.didReceiveCellUpdate(cell)); @@ -159,8 +159,8 @@ class DateCalBloc extends Bloc { TimeFormat? timeFormat, bool? includeTime, }) async { - state.dateTypeOption.freeze(); - final newDateTypeOption = state.dateTypeOption.rebuild((typeOption) { + state.dateTypeOptionPB.freeze(); + final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) { if (dateFormat != null) { typeOption.dateFormat = dateFormat; } @@ -175,14 +175,14 @@ class DateCalBloc extends Bloc { }); final result = await FieldService.updateFieldTypeOption( - gridId: cellContext.gridId, - fieldId: cellContext.field.id, + gridId: cellController.gridId, + fieldId: cellController.field.id, typeOptionData: newDateTypeOption.writeToBuffer(), ); result.fold( (l) => emit(state.copyWith( - dateTypeOption: newDateTypeOption, + dateTypeOptionPB: newDateTypeOption, timeHintText: _timeHintText(newDateTypeOption))), (err) => Log.error(err), ); @@ -210,7 +210,7 @@ class DateCalEvent with _$DateCalEvent { @freezed class DateCalState with _$DateCalState { const factory DateCalState({ - required DateTypeOption dateTypeOption, + required DateTypeOptionPB dateTypeOptionPB, required CalendarFormat format, required DateTime focusedDay, required Option timeFormatError, @@ -220,24 +220,24 @@ class DateCalState with _$DateCalState { }) = _DateCalState; factory DateCalState.initial( - DateTypeOption dateTypeOption, + DateTypeOptionPB dateTypeOptionPB, DateCellDataPB? cellData, ) { Option calData = calDataFromCellData(cellData); final time = calData.foldRight("", (dateData, previous) => dateData.time); return DateCalState( - dateTypeOption: dateTypeOption, + dateTypeOptionPB: dateTypeOptionPB, format: CalendarFormat.month, focusedDay: DateTime.now(), time: time, calData: calData, timeFormatError: none(), - timeHintText: _timeHintText(dateTypeOption), + timeHintText: _timeHintText(dateTypeOptionPB), ); } } -String _timeHintText(DateTypeOption typeOption) { +String _timeHintText(DateTypeOptionPB typeOption) { switch (typeOption.timeFormat) { case TimeFormat.TwelveHour: return LocaleKeys.document_date_timeHintTextInTwelveHour.tr(); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart index fdc0ad1082..4150093275 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart @@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart'; part 'date_cell_bloc.freezed.dart'; class DateCellBloc extends Bloc { - final GridDateCellController cellContext; + final GridDateCellController cellController; void Function()? _onCellChangedFn; - DateCellBloc({required this.cellContext}) - : super(DateCellState.initial(cellContext)) { + DateCellBloc({required this.cellController}) + : super(DateCellState.initial(cellController)) { on( (event, emit) async { event.when( @@ -30,15 +30,15 @@ class DateCellBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((data) { if (!isClosed) { add(DateCellEvent.didReceiveCellUpdate(data)); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart index 65eec13e6c..88b28ea414 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart @@ -8,12 +8,12 @@ import 'cell_service/cell_service.dart'; part 'number_cell_bloc.freezed.dart'; class NumberCellBloc extends Bloc { - final GridCellController cellContext; + final GridCellController cellController; void Function()? _onCellChangedFn; NumberCellBloc({ - required this.cellContext, - }) : super(NumberCellState.initial(cellContext)) { + required this.cellController, + }) : super(NumberCellState.initial(cellController)) { on( (event, emit) async { event.when( @@ -24,11 +24,13 @@ class NumberCellBloc extends Bloc { emit(state.copyWith(content: content)); }, updateCell: (text) { - cellContext.saveCellData(text, resultCallback: (result) { + cellController.saveCellData(text, resultCallback: (result) { result.fold( () => null, (err) { - if (!isClosed) add(NumberCellEvent.didReceiveCellUpdate(right(err))); + if (!isClosed) { + add(NumberCellEvent.didReceiveCellUpdate(right(err))); + } }, ); }); @@ -41,15 +43,15 @@ class NumberCellBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((cellContent) { if (!isClosed) { add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? ""))); @@ -63,7 +65,8 @@ class NumberCellBloc extends Bloc { class NumberCellEvent with _$NumberCellEvent { const factory NumberCellEvent.initial() = _Initial; const factory NumberCellEvent.updateCell(String text) = _UpdateCell; - const factory NumberCellEvent.didReceiveCellUpdate(Either cellContent) = _DidReceiveCellUpdate; + const factory NumberCellEvent.didReceiveCellUpdate( + Either cellContent) = _DidReceiveCellUpdate; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_cell_bloc.dart index d823568e17..fca90c9903 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_cell_bloc.dart @@ -8,12 +8,12 @@ part 'select_option_cell_bloc.freezed.dart'; class SelectOptionCellBloc extends Bloc { - final GridSelectOptionCellController cellContext; + final GridSelectOptionCellController cellController; void Function()? _onCellChangedFn; SelectOptionCellBloc({ - required this.cellContext, - }) : super(SelectOptionCellState.initial(cellContext)) { + required this.cellController, + }) : super(SelectOptionCellState.initial(cellController)) { on( (event, emit) async { await event.map( @@ -33,15 +33,15 @@ class SelectOptionCellBloc @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((selectOptionContext) { if (!isClosed) { add(SelectOptionCellEvent.didReceiveOptions( diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/text_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/text_cell_bloc.dart index 783564b5fa..3fa55b744f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/text_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/text_cell_bloc.dart @@ -6,11 +6,11 @@ import 'cell_service/cell_service.dart'; part 'text_cell_bloc.freezed.dart'; class TextCellBloc extends Bloc { - final GridCellController cellContext; + final GridCellController cellController; void Function()? _onCellChangedFn; TextCellBloc({ - required this.cellContext, - }) : super(TextCellState.initial(cellContext)) { + required this.cellController, + }) : super(TextCellState.initial(cellController)) { on( (event, emit) async { await event.when( @@ -18,7 +18,7 @@ class TextCellBloc extends Bloc { _startListening(); }, updateText: (text) { - cellContext.saveCellData(text); + cellController.saveCellData(text); emit(state.copyWith(content: text)); }, didReceiveCellUpdate: (content) { @@ -32,15 +32,15 @@ class TextCellBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((cellContent) { if (!isClosed) { add(TextCellEvent.didReceiveCellUpdate(cellContent ?? "")); @@ -53,7 +53,8 @@ class TextCellBloc extends Bloc { @freezed class TextCellEvent with _$TextCellEvent { const factory TextCellEvent.initial() = _InitialCell; - const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate; + const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; const factory TextCellEvent.updateText(String text) = _UpdateText; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_bloc.dart index e43f561542..824900f173 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_bloc.dart @@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart'; part 'url_cell_bloc.freezed.dart'; class URLCellBloc extends Bloc { - final GridURLCellController cellContext; + final GridURLCellController cellController; void Function()? _onCellChangedFn; URLCellBloc({ - required this.cellContext, - }) : super(URLCellState.initial(cellContext)) { + required this.cellController, + }) : super(URLCellState.initial(cellController)) { on( (event, emit) async { event.when( @@ -25,7 +25,7 @@ class URLCellBloc extends Bloc { )); }, updateURL: (String url) { - cellContext.saveCellData(url, deduplicate: true); + cellController.saveCellData(url, deduplicate: true); }, ); }, @@ -35,15 +35,15 @@ class URLCellBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((cellData) { if (!isClosed) { add(URLCellEvent.didReceiveCellUpdate(cellData)); @@ -57,7 +57,8 @@ class URLCellBloc extends Bloc { class URLCellEvent with _$URLCellEvent { const factory URLCellEvent.initial() = _InitialCell; const factory URLCellEvent.updateURL(String url) = _UpdateURL; - const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate; + const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; } @freezed diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart index 067be84b7b..8e82c27f42 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/url_cell_editor_bloc.dart @@ -7,11 +7,11 @@ import 'cell_service/cell_service.dart'; part 'url_cell_editor_bloc.freezed.dart'; class URLCellEditorBloc extends Bloc { - final GridURLCellController cellContext; + final GridURLCellController cellController; void Function()? _onCellChangedFn; URLCellEditorBloc({ - required this.cellContext, - }) : super(URLCellEditorState.initial(cellContext)) { + required this.cellController, + }) : super(URLCellEditorState.initial(cellController)) { on( (event, emit) async { event.when( @@ -19,7 +19,7 @@ class URLCellEditorBloc extends Bloc { _startListening(); }, updateText: (text) { - cellContext.saveCellData(text, deduplicate: true); + cellController.saveCellData(text, deduplicate: true); emit(state.copyWith(content: text)); }, didReceiveCellUpdate: (cellData) { @@ -33,15 +33,15 @@ class URLCellEditorBloc extends Bloc { @override Future close() async { if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); + cellController.removeListener(_onCellChangedFn!); _onCellChangedFn = null; } - cellContext.dispose(); + cellController.dispose(); return super.close(); } void _startListening() { - _onCellChangedFn = cellContext.startListening( + _onCellChangedFn = cellController.startListening( onCellChanged: ((cellData) { if (!isClosed) { add(URLCellEditorEvent.didReceiveCellUpdate(cellData)); @@ -54,7 +54,8 @@ class URLCellEditorBloc extends Bloc { @freezed class URLCellEditorEvent with _$URLCellEditorEvent { const factory URLCellEditorEvent.initial() = _InitialCell; - const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate; + const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; const factory URLCellEditorEvent.updateText(String text) = _UpdateText; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart index 2b2a853bba..e45ed58a2c 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/date_bloc.dart @@ -32,7 +32,7 @@ class DateTypeOptionBloc ); } - DateTypeOption _updateTypeOption({ + DateTypeOptionPB _updateTypeOption({ DateFormat? dateFormat, TimeFormat? timeFormat, bool? includeTime, @@ -72,9 +72,9 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent { @freezed class DateTypeOptionState with _$DateTypeOptionState { const factory DateTypeOptionState({ - required DateTypeOption typeOption, + required DateTypeOptionPB typeOption, }) = _DateTypeOptionState; - factory DateTypeOptionState.initial(DateTypeOption typeOption) => + factory DateTypeOptionState.initial(DateTypeOptionPB typeOption) => DateTypeOptionState(typeOption: typeOption); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart index d53c5b9cda..a475652095 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/number_bloc.dart @@ -23,7 +23,7 @@ class NumberTypeOptionBloc ); } - NumberTypeOption _updateNumberFormat(NumberFormat format) { + NumberTypeOptionPB _updateNumberFormat(NumberFormat format) { state.typeOption.freeze(); return state.typeOption.rebuild((typeOption) { typeOption.format = format; @@ -45,10 +45,10 @@ class NumberTypeOptionEvent with _$NumberTypeOptionEvent { @freezed class NumberTypeOptionState with _$NumberTypeOptionState { const factory NumberTypeOptionState({ - required NumberTypeOption typeOption, + required NumberTypeOptionPB typeOption, }) = _NumberTypeOptionState; - factory NumberTypeOptionState.initial(NumberTypeOption typeOption) => + factory NumberTypeOptionState.initial(NumberTypeOptionPB typeOption) => NumberTypeOptionState( typeOption: typeOption, ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart index 9b04fcbed9..632e333911 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_context.dart @@ -18,56 +18,56 @@ abstract class TypeOptionDataParser { } // Number -typedef NumberTypeOptionContext = TypeOptionContext; +typedef NumberTypeOptionContext = TypeOptionContext; class NumberTypeOptionWidgetDataParser - extends TypeOptionDataParser { + extends TypeOptionDataParser { @override - NumberTypeOption fromBuffer(List buffer) { - return NumberTypeOption.fromBuffer(buffer); + NumberTypeOptionPB fromBuffer(List buffer) { + return NumberTypeOptionPB.fromBuffer(buffer); } } // RichText -typedef RichTextTypeOptionContext = TypeOptionContext; +typedef RichTextTypeOptionContext = TypeOptionContext; class RichTextTypeOptionWidgetDataParser - extends TypeOptionDataParser { + extends TypeOptionDataParser { @override - RichTextTypeOption fromBuffer(List buffer) { - return RichTextTypeOption.fromBuffer(buffer); + RichTextTypeOptionPB fromBuffer(List buffer) { + return RichTextTypeOptionPB.fromBuffer(buffer); } } // Checkbox -typedef CheckboxTypeOptionContext = TypeOptionContext; +typedef CheckboxTypeOptionContext = TypeOptionContext; class CheckboxTypeOptionWidgetDataParser - extends TypeOptionDataParser { + extends TypeOptionDataParser { @override - CheckboxTypeOption fromBuffer(List buffer) { - return CheckboxTypeOption.fromBuffer(buffer); + CheckboxTypeOptionPB fromBuffer(List buffer) { + return CheckboxTypeOptionPB.fromBuffer(buffer); } } // URL -typedef URLTypeOptionContext = TypeOptionContext; +typedef URLTypeOptionContext = TypeOptionContext; class URLTypeOptionWidgetDataParser - extends TypeOptionDataParser { + extends TypeOptionDataParser { @override - URLTypeOption fromBuffer(List buffer) { - return URLTypeOption.fromBuffer(buffer); + URLTypeOptionPB fromBuffer(List buffer) { + return URLTypeOptionPB.fromBuffer(buffer); } } // Date -typedef DateTypeOptionContext = TypeOptionContext; +typedef DateTypeOptionContext = TypeOptionContext; -class DateTypeOptionDataParser extends TypeOptionDataParser { +class DateTypeOptionDataParser extends TypeOptionDataParser { @override - DateTypeOption fromBuffer(List buffer) { - return DateTypeOption.fromBuffer(buffer); + DateTypeOptionPB fromBuffer(List buffer) { + return DateTypeOptionPB.fromBuffer(buffer); } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart index 94e8ceeb71..fa5be5f9bd 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_cell.dart @@ -84,7 +84,7 @@ class _DateCellState extends GridCellState { DateCellEditor(onDismissed: () => widget.onCellEditing.value = false); calendar.show( context, - cellController: bloc.cellContext.clone(), + cellController: bloc.cellController.clone(), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart index bfcc36439c..4ab3ff352d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart @@ -40,10 +40,10 @@ class DateCellEditor with FlowyOverlayDelegate { final result = await cellController.getFieldTypeOption(DateTypeOptionDataParser()); result.fold( - (dateTypeOption) { + (dateTypeOptionPB) { final calendar = _CellCalendarWidget( cellContext: cellController, - dateTypeOption: dateTypeOption, + dateTypeOptionPB: dateTypeOptionPB, ); FlowyOverlay.of(context).insertWithAnchor( @@ -79,11 +79,11 @@ class DateCellEditor with FlowyOverlayDelegate { class _CellCalendarWidget extends StatelessWidget { final GridDateCellController cellContext; - final DateTypeOption dateTypeOption; + final DateTypeOptionPB dateTypeOptionPB; const _CellCalendarWidget({ required this.cellContext, - required this.dateTypeOption, + required this.dateTypeOptionPB, Key? key, }) : super(key: key); @@ -93,9 +93,9 @@ class _CellCalendarWidget extends StatelessWidget { return BlocProvider( create: (context) { return DateCalBloc( - dateTypeOption: dateTypeOption, + dateTypeOptionPB: dateTypeOptionPB, cellData: cellContext.getCellData(), - cellContext: cellContext, + cellController: cellContext, )..add(const DateCalEvent.initial()); }, child: BlocBuilder( @@ -197,7 +197,7 @@ class _IncludeTimeButton extends StatelessWidget { Widget build(BuildContext context) { final theme = context.watch(); return BlocSelector( - selector: (state) => state.dateTypeOption.includeTime, + selector: (state) => state.dateTypeOptionPB.includeTime, builder: (context, includeTime) { return SizedBox( height: 50, @@ -244,7 +244,7 @@ class _TimeTextFieldState extends State<_TimeTextField> { void initState() { _focusNode = FocusNode(); _controller = TextEditingController(text: widget.bloc.state.time); - if (widget.bloc.state.dateTypeOption.includeTime) { + if (widget.bloc.state.dateTypeOptionPB.includeTime) { _focusNode.addListener(() { if (mounted) { _CalDateTimeSetting.hide(context); @@ -265,7 +265,7 @@ class _TimeTextFieldState extends State<_TimeTextField> { }, listenWhen: (p, c) => p.time != c.time, builder: (context, state) { - if (state.dateTypeOption.includeTime) { + if (state.dateTypeOptionPB.includeTime) { return Padding( padding: kMargin, child: RoundedInputField( @@ -307,23 +307,24 @@ class _DateTypeOptionButton extends StatelessWidget { final title = LocaleKeys.grid_field_dateFormat.tr() + " &" + LocaleKeys.grid_field_timeFormat.tr(); - return BlocSelector( - selector: (state) => state.dateTypeOption, - builder: (context, dateTypeOption) { + return BlocSelector( + selector: (state) => state.dateTypeOptionPB, + builder: (context, dateTypeOptionPB) { return FlowyButton( text: FlowyText.medium(title, fontSize: 12), hoverColor: theme.hover, margin: kMargin, - onTap: () => _showTimeSetting(dateTypeOption, context), + onTap: () => _showTimeSetting(dateTypeOptionPB, context), rightIcon: svgWidget("grid/more", color: theme.iconColor), ); }, ); } - void _showTimeSetting(DateTypeOption dateTypeOption, BuildContext context) { + void _showTimeSetting( + DateTypeOptionPB dateTypeOptionPB, BuildContext context) { final setting = _CalDateTimeSetting( - dateTypeOption: dateTypeOption, + dateTypeOptionPB: dateTypeOptionPB, onEvent: (event) => context.read().add(event), ); setting.show(context); @@ -331,10 +332,10 @@ class _DateTypeOptionButton extends StatelessWidget { } class _CalDateTimeSetting extends StatefulWidget { - final DateTypeOption dateTypeOption; + final DateTypeOptionPB dateTypeOptionPB; final Function(DateCalEvent) onEvent; const _CalDateTimeSetting( - {required this.dateTypeOption, required this.onEvent, Key? key}) + {required this.dateTypeOptionPB, required this.onEvent, Key? key}) : super(key: key); @override @@ -371,17 +372,17 @@ class _CalDateTimeSettingState extends State<_CalDateTimeSetting> { List children = [ DateFormatButton(onTap: () { final list = DateFormatList( - selectedFormat: widget.dateTypeOption.dateFormat, + selectedFormat: widget.dateTypeOptionPB.dateFormat, onSelected: (format) => widget.onEvent(DateCalEvent.setDateFormat(format)), ); _showOverlay(context, list); }), TimeFormatButton( - timeFormat: widget.dateTypeOption.timeFormat, + timeFormat: widget.dateTypeOptionPB.timeFormat, onTap: () { final list = TimeFormatList( - selectedFormat: widget.dateTypeOption.timeFormat, + selectedFormat: widget.dateTypeOptionPB.timeFormat, onSelected: (format) => widget.onEvent(DateCalEvent.setTimeFormat(format)), ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index a4e09ec0b9..1bd54e7514 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -59,7 +59,7 @@ class _SingleSelectCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - return _SelectOptionCell( + return SelectOptionWrap( selectOptions: state.selectedOptions, cellStyle: widget.cellStyle, onFocus: (value) => widget.onCellEditing.value = value, @@ -115,11 +115,12 @@ class _MultiSelectCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - return _SelectOptionCell( - selectOptions: state.selectedOptions, - cellStyle: widget.cellStyle, - onFocus: (value) => widget.onCellEditing.value = value, - cellControllerBuilder: widget.cellControllerBuilder); + return SelectOptionWrap( + selectOptions: state.selectedOptions, + cellStyle: widget.cellStyle, + onFocus: (value) => widget.onCellEditing.value = value, + cellControllerBuilder: widget.cellControllerBuilder, + ); }, ), ); @@ -132,16 +133,16 @@ class _MultiSelectCellState extends State { } } -class _SelectOptionCell extends StatelessWidget { +class SelectOptionWrap extends StatelessWidget { final List selectOptions; - final void Function(bool) onFocus; + final void Function(bool)? onFocus; final SelectOptionCellStyle? cellStyle; final GridCellControllerBuilder cellControllerBuilder; - const _SelectOptionCell({ + const SelectOptionWrap({ required this.selectOptions, - required this.onFocus, - required this.cellStyle, required this.cellControllerBuilder, + this.onFocus, + this.cellStyle, Key? key, }) : super(key: key); @@ -177,11 +178,11 @@ class _SelectOptionCell extends StatelessWidget { child, InkWell( onTap: () { - onFocus(true); + onFocus?.call(true); final cellContext = cellControllerBuilder.build() as GridSelectOptionCellController; SelectOptionCellEditor.show( - context, cellContext, () => onFocus(false)); + context, cellContext, () => onFocus?.call(false)); }, ), ], diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart index 9095d73fa6..e68ac720a3 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/cell_editor.dart @@ -64,7 +64,7 @@ class _URLCellEditorState extends State { @override void initState() { - _cellBloc = URLCellEditorBloc(cellContext: widget.cellController); + _cellBloc = URLCellEditorBloc(cellController: widget.cellController); _cellBloc.add(const URLCellEditorEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index 7295228daf..585ef1ce85 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -92,7 +92,7 @@ class _GridURLCellState extends GridCellState { void initState() { final cellContext = widget.cellControllerBuilder.build() as GridURLCellController; - _cellBloc = URLCellBloc(cellContext: cellContext); + _cellBloc = URLCellBloc(cellController: cellContext); _cellBloc.add(const URLCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index 77b4981939..2ea19eb1e8 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -64,7 +64,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ switch (dataController.field.fieldType) { case FieldType.Checkbox: return CheckboxTypeOptionWidgetBuilder( - makeTypeOptionContextWithDataController( + makeTypeOptionContextWithDataController( gridId: gridId, fieldType: fieldType, dataController: dataController, @@ -72,7 +72,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ ); case FieldType.DateTime: return DateTypeOptionWidgetBuilder( - makeTypeOptionContextWithDataController( + makeTypeOptionContextWithDataController( gridId: gridId, fieldType: fieldType, dataController: dataController, @@ -99,7 +99,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ ); case FieldType.Number: return NumberTypeOptionWidgetBuilder( - makeTypeOptionContextWithDataController( + makeTypeOptionContextWithDataController( gridId: gridId, fieldType: fieldType, dataController: dataController, @@ -108,7 +108,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ ); case FieldType.RichText: return RichTextTypeOptionWidgetBuilder( - makeTypeOptionContextWithDataController( + makeTypeOptionContextWithDataController( gridId: gridId, fieldType: fieldType, dataController: dataController, @@ -117,7 +117,7 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ case FieldType.URL: return URLTypeOptionWidgetBuilder( - makeTypeOptionContextWithDataController( + makeTypeOptionContextWithDataController( gridId: gridId, fieldType: fieldType, dataController: dataController, diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 7062a9f3a8..be3c617943 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -170,33 +170,33 @@ void _resolveGridDeps(GetIt getIt) { getIt.registerFactoryParam( (context, _) => TextCellBloc( - cellContext: context, + cellController: context, ), ); getIt.registerFactoryParam( (context, _) => SelectOptionCellBloc( - cellContext: context, + cellController: context, ), ); getIt.registerFactoryParam( (context, _) => NumberCellBloc( - cellContext: context, + cellController: context, ), ); getIt.registerFactoryParam( (context, _) => DateCellBloc( - cellContext: context, + cellController: context, ), ); getIt.registerFactoryParam( (cellData, _) => CheckboxCellBloc( service: CellService(), - cellContext: cellData, + cellController: cellData, ), ); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index eb16ba09ba..282e6028c5 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -87,7 +87,7 @@ class _MultiBoardListExampleState extends State { ); } - Widget _buildCard(ColumnItem item) { + Widget _buildCard(AFColumnItem item) { if (item is TextItem) { return Align( alignment: Alignment.centerLeft, @@ -126,7 +126,7 @@ class _MultiBoardListExampleState extends State { } } -class TextItem extends ColumnItem { +class TextItem extends AFColumnItem { final String s; TextItem(this.s); @@ -135,7 +135,7 @@ class TextItem extends ColumnItem { String get id => s; } -class RichTextItem extends ColumnItem { +class RichTextItem extends AFColumnItem { final String title; final String subtitle; diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart index 8c4226a65c..97e83df448 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/single_board_list_example.dart @@ -50,7 +50,7 @@ class _RowWidget extends StatelessWidget { } } -class TextItem extends ColumnItem { +class TextItem extends AFColumnItem { final String s; TextItem(this.s); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index 09d441bc16..3873bc222d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -24,7 +24,7 @@ typedef OnColumnInserted = void Function(String listId, int insertedIndex); typedef AFBoardColumnCardBuilder = Widget Function( BuildContext context, - ColumnItem item, + AFColumnItem item, ); typedef AFBoardColumnHeaderBuilder = Widget Function( @@ -46,7 +46,7 @@ abstract class AFBoardColumnDataDataSource extends ReoderFlextDataSource { String get identifier => columnData.id; @override - UnmodifiableListView get items => columnData.items; + UnmodifiableListView get items => columnData.items; void debugPrint() { String msg = '[$AFBoardColumnDataDataSource] $columnData data: '; @@ -194,7 +194,7 @@ class _AFBoardColumnWidgetState extends State { ); } - Widget _buildWidget(BuildContext context, ColumnItem item) { + Widget _buildWidget(BuildContext context, AFColumnItem item) { if (item is PhantomColumnItem) { return PassthroughPhantomWidget( key: UniqueKey(), diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index b7ef467a23..cfce93af00 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import '../../utils/log.dart'; import '../reorder_flex/reorder_flex.dart'; -abstract class ColumnItem extends ReoderFlexItem { +abstract class AFColumnItem extends ReoderFlexItem { bool get isPhantom => false; @override @@ -31,7 +31,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { List get props => columnData.props; /// Returns the readonly List - UnmodifiableListView get items => + UnmodifiableListView get items => UnmodifiableListView(columnData.items); /// Remove the item at [index]. @@ -39,7 +39,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { /// * [notify] the default value of [notify] is true, it will notify the /// listener. Set to [false] if you do not want to notify the listeners. /// - ColumnItem removeAt(int index, {bool notify = true}) { + AFColumnItem removeAt(int index, {bool notify = true}) { assert(index >= 0); Log.debug('[$BoardColumnDataController] $columnData remove item at $index'); @@ -50,7 +50,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { return item; } - int removeWhere(bool Function(ColumnItem) condition) { + int removeWhere(bool Function(AFColumnItem) condition) { return items.indexWhere(condition); } @@ -75,7 +75,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { /// is true. /// /// The default value of [notify] is true. - bool insert(int index, ColumnItem item, {bool notify = true}) { + bool insert(int index, AFColumnItem item, {bool notify = true}) { assert(index >= 0); Log.debug( '[$BoardColumnDataController] $columnData insert $item at $index'); @@ -90,14 +90,14 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { return true; } - bool add(ColumnItem item, {bool notify = true}) { + bool add(AFColumnItem item, {bool notify = true}) { columnData._items.add(item); if (notify) notifyListeners(); return true; } /// Replace the item at index with the [newItem]. - void replace(int index, ColumnItem newItem) { + void replace(int index, AFColumnItem newItem) { if (columnData._items.isEmpty) { columnData._items.add(newItem); Log.debug('[$BoardColumnDataController] $columnData add $newItem'); @@ -117,18 +117,18 @@ class AFBoardColumnData extends ReoderFlexItem with EquatableMixin { @override final String id; final String desc; - final List _items; + final List _items; final CustomData? customData; AFBoardColumnData({ this.customData, required this.id, this.desc = "", - List items = const [], + List items = const [], }) : _items = items; /// Returns the readonly List - UnmodifiableListView get items => UnmodifiableListView(_items); + UnmodifiableListView get items => UnmodifiableListView(_items); @override List get props => [id, ..._items]; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 60bde5bc79..00c84daa3c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -110,11 +110,11 @@ class AFBoardDataController extends ChangeNotifier } } - void addColumnItem(String columnId, ColumnItem item) { + void addColumnItem(String columnId, AFColumnItem item) { getColumnController(columnId)?.add(item); } - void insertColumnItem(String columnId, int index, ColumnItem item) { + void insertColumnItem(String columnId, int index, AFColumnItem item) { getColumnController(columnId)?.insert(index, item); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index ccff83b502..bd770fa820 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -253,7 +253,7 @@ class PhantomRecord { } } -class PhantomColumnItem extends ColumnItem { +class PhantomColumnItem extends AFColumnItem { final PassthroughPhantomContext phantomContext; PhantomColumnItem(PassthroughPhantomContext insertedPhantom) @@ -290,7 +290,7 @@ class PassthroughPhantomContext extends FakeDragTargetEventTrigger Widget? get draggingWidget => dragTargetData.draggingWidget; - ColumnItem get itemData => dragTargetData.reorderFlexItem as ColumnItem; + AFColumnItem get itemData => dragTargetData.reorderFlexItem as AFColumnItem; @override VoidCallback? onInserted; diff --git a/frontend/rust-lib/flowy-test/src/event_builder.rs b/frontend/rust-lib/flowy-test/src/event_builder.rs index d9841d6b4a..d6f42ecf31 100644 --- a/frontend/rust-lib/flowy-test/src/event_builder.rs +++ b/frontend/rust-lib/flowy-test/src/event_builder.rs @@ -86,9 +86,13 @@ where match response.parse::() { Ok(Ok(data)) => data, Ok(Err(e)) => { - panic!("parse failed: {:?}", e) + panic!("Parser {:?} failed: {:?}", std::any::type_name::(), e) } - Err(e) => panic!("Internal error: {:?}", e), + Err(e) => panic!( + "Internal error: {:?}, parser {:?} failed", + e, + std::any::type_name::(), + ), } } From 53cb05998bd3ae6f091a433830e0093176b97ffc Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 16:37:43 +0800 Subject: [PATCH 100/224] feat(doc): HTML converter --- .../lib/src/infra/html_converter.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index cc7fd949ae..4a15b54859 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -484,6 +484,22 @@ class NodesToHTMLConverter { }); } + /// Convert the rich text to HTML + /// + /// Use `` for bold only. + /// Use `` for italic only. + /// Use `` for strikethrough only. + /// Use `` for underline only. + /// + /// If the text has multiple styles, use a `` + /// to mix the styles. + /// + /// A CSS style string is used to describe the styles. + /// The HTML will be: + /// + /// ```html + /// Text + /// ``` html.Element _deltaToHtml(Delta delta, {String? subType, int? end, bool? checked}) { if (end != null) { From 0424c14c7d99fa9128ca08f3319d9c8b396374ca Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 17:58:16 +0800 Subject: [PATCH 101/224] feat: deep clone nodes --- .../flowy_editor/lib/src/document/node.dart | 28 +++++++++++++++++++ .../src/operation/transaction_builder.dart | 4 +-- .../copy_paste_handler.dart | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 24c3035e90..6bfc877524 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -163,6 +163,18 @@ class Node extends ChangeNotifier with LinkedListEntry { } return parent!._path([index, ...previous]); } + + Node deepClone() { + final newNode = Node( + type: type, children: LinkedList(), attributes: {...attributes}); + + for (final node in children) { + final newNode = node.deepClone(); + newNode.parent = this; + newNode.children.add(newNode); + } + return newNode; + } } class TextNode extends Node { @@ -213,5 +225,21 @@ class TextNode extends Node { delta: delta ?? this.delta, ); + @override + TextNode deepClone() { + final newNode = TextNode( + type: type, + children: LinkedList(), + delta: delta.slice(0), + attributes: {...attributes}); + + for (final node in children) { + final newNode = node.deepClone(); + newNode.parent = this; + newNode.children.add(newNode); + } + return newNode; + } + String toRawString() => _delta.toRawString(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index f8cc80b161..dfca3cd661 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -36,7 +36,7 @@ class TransactionBuilder { /// Insert a sequence of nodes at the position of path. insertNodes(Path path, List nodes) { beforeSelection = state.cursorSelection; - add(InsertOperation(path, nodes)); + add(InsertOperation(path, nodes.map((node) => node.deepClone()).toList())); } /// Update the attributes of nodes. @@ -75,7 +75,7 @@ class TransactionBuilder { nodes.add(node); } - add(DeleteOperation(path, nodes)); + add(DeleteOperation(path, nodes.map((node) => node.deepClone()).toList())); } textEdit(TextNode node, Delta Function() f) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 363b84967a..b1bf170672 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -222,7 +222,7 @@ _handleCut(EditorState editorState) { } _deleteSelectedContent(EditorState editorState) { - final selection = editorState.cursorSelection; + final selection = editorState.cursorSelection?.normalize(); if (selection == null) { return; } From ba05aa137ecb9ef514f240cfb3d511957d02b08f Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 18:03:22 +0800 Subject: [PATCH 102/224] feat: filter the empty delta --- .../packages/flowy_editor/lib/src/document/text_delta.dart | 3 ++- .../flowy_editor/lib/src/operation/transaction_builder.dart | 3 +++ .../enter_without_shift_in_text_node_handler.dart | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart index 05f0d61b8d..658114df7a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/text_delta.dart @@ -417,7 +417,8 @@ class Delta extends Iterable { // Optimization if rest of other is just retain if (!otherIter.hasNext && - delta._operations[delta._operations.length - 1] == newOp) { + delta._operations.isNotEmpty && + delta._operations.last == newOp) { final rest = Delta(thisIter.rest()); return (delta + rest)..chop(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart index dfca3cd661..68a46d8a93 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/operation/transaction_builder.dart @@ -203,6 +203,9 @@ class TransactionBuilder { for (var i = 0; i < operations.length; i++) { op = transformOperation(operations[i], op); } + if (op is TextEditOperation && op.delta.isEmpty) { + return; + } operations.add(op); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 39c74d2eab..1a25a2531d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -86,7 +86,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = ); TransactionBuilder(editorState) ..insertNode( - textNode.path, + textNode.path.next, TextNode.empty(), ) ..afterSelection = afterSelection From d822e1e2ca15d58181638fd816ed9d79331f09bd Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Fri, 12 Aug 2022 18:44:56 +0800 Subject: [PATCH 103/224] fix: paste nodes --- .../copy_paste_handler.dart | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index b1bf170672..7c89c2ed83 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -6,11 +6,10 @@ import 'package:flutter/services.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; _handleCopy(EditorState editorState) async { - var selection = editorState.cursorSelection; + final selection = editorState.cursorSelection?.normalize(); if (selection == null || selection.isCollapsed) { return; } - selection = selection.normalize(); if (pathEquals(selection.start.path, selection.end.path)) { final nodeAtPath = editorState.document.nodeAtPath(selection.end.path)!; if (nodeAtPath.type == "text") { @@ -43,11 +42,13 @@ _handleCopy(EditorState editorState) async { } _pasteHTML(EditorState editorState, String html) { - final selection = editorState.cursorSelection; + final selection = editorState.cursorSelection?.normalize(); if (selection == null) { return; } + assert(selection.isCollapsed); + final path = [...selection.end.path]; if (path.isEmpty) { return; @@ -124,6 +125,20 @@ _pasteMultipleLinesInText( _handlePaste(EditorState editorState) async { final data = await RichClipboard.getData(); + + if (editorState.cursorSelection?.isCollapsed ?? false) { + _pastRichClipboard(editorState, data); + return; + } + + _deleteSelectedContent(editorState); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _pastRichClipboard(editorState, data); + }); +} + +_pastRichClipboard(EditorState editorState, RichClipboardData data) { if (data.html != null) { _pasteHTML(editorState, data.html!); return; @@ -135,7 +150,7 @@ _handlePaste(EditorState editorState) async { } _handlePastePlainText(EditorState editorState, String plainText) { - final selection = editorState.cursorSelection; + final selection = editorState.cursorSelection?.normalize(); if (selection == null) { return; } @@ -208,22 +223,13 @@ _handlePastePlainText(EditorState editorState, String plainText) { /// 2. delete selected content _handleCut(EditorState editorState) { debugPrint('cut'); - final selection = editorState.cursorSelection; - if (selection == null) { - return; - } - - if (selection.isCollapsed) { - return; - } - _handleCopy(editorState); _deleteSelectedContent(editorState); } _deleteSelectedContent(EditorState editorState) { final selection = editorState.cursorSelection?.normalize(); - if (selection == null) { + if (selection == null || selection.isCollapsed) { return; } final beginNode = editorState.document.nodeAtPath(selection.start.path)!; @@ -262,11 +268,11 @@ _deleteSelectedContent(EditorState editorState) { return delta; }); - tb.setAfterSelection(Selection.collapsed(selection.start)); } else { tb.deleteNode(item); } } + tb.setAfterSelection(Selection.collapsed(selection.start)); tb.commit(); } From 4488247fb173c178a5e3585e9e7607e946968a49 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 19:19:19 +0800 Subject: [PATCH 104/224] fix: default icon for user avatar --- .../lib/workspace/presentation/home/menu/menu_user.dart | 4 ++++ .../presentation/settings/widgets/settings_user_view.dart | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index baf9dbffab..3d9d76fe29 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -1,6 +1,7 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:app_flowy/workspace/presentation/settings/settings_dialog.dart'; +import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; @@ -41,6 +42,9 @@ class MenuUser extends StatelessWidget { Widget _renderAvatar(BuildContext context) { String iconUrl = context.read().state.userProfile.iconUrl; + if (iconUrl.isEmpty) { + iconUrl = defaultUserAvatar; + } return SizedBox( width: 25, diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart index a235fe7dcf..ceabdd9b7b 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -8,6 +8,8 @@ import 'package:flowy_infra/image.dart'; import 'dart:convert'; +const defaultUserAvatar = '1F600'; + class SettingsUserView extends StatelessWidget { final UserProfilePB user; SettingsUserView(this.user, {Key? key}) : super(key: ValueKey(user.id)); @@ -40,6 +42,9 @@ class SettingsUserView extends StatelessWidget { Widget _renderCurrentIcon(BuildContext context) { String iconUrl = context.read().state.userProfile.iconUrl; + if (iconUrl.isEmpty) { + iconUrl = defaultUserAvatar; + } return _CurrentIcon(iconUrl); } } From 800fb822115273021036e7eba38fa2873dc4deef Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 20:10:56 +0800 Subject: [PATCH 105/224] chore: card ui --- .../plugins/board/application/board_bloc.dart | 34 ++++-- .../application/board_data_controller.dart | 46 +++++++- .../card/board_date_cell_bloc.dart | 0 .../board/application/card/card_bloc.dart | 111 ++++++++++++++++++ .../card/card_data_controller.dart | 49 ++++++++ .../app_flowy/lib/plugins/board/board.dart | 2 +- .../board/presentation/board_page.dart | 26 +++- .../card/board_checkbox_cell.dart | 21 ++++ .../presentation/card/board_date_cell.dart | 21 ++++ .../presentation/card/board_number_cell.dart | 21 ++++ .../presentation/card/board_url_cell.dart | 21 ++++ .../plugins/board/presentation/card/card.dart | 60 +++++++++- .../presentation/card/card_cell_builder.dart | 69 +++++++++++ .../application/grid_data_controller.dart | 2 +- .../row/row_action_sheet_bloc.dart | 4 +- .../grid/application/row/row_bloc.dart | 16 +-- .../application/row/row_data_controller.dart | 6 +- .../grid/application/row/row_service.dart | 4 +- .../presentation/widgets/row/grid_row.dart | 2 +- .../presentation/home/menu/menu_user.dart | 2 +- .../flowy-folder/tests/workspace/script.rs | 10 +- 21 files changed, 480 insertions(+), 47 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index df79e3154c..37487a4eec 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:dartz/dartz.dart'; @@ -20,6 +21,9 @@ class BoardBloc extends Bloc { final BoardDataController _dataController; late final AFBoardDataController boardDataController; + GridFieldCache get fieldCache => _dataController.fieldCache; + String get gridId => _dataController.gridId; + BoardBloc({required ViewPB view}) : _dataController = BoardDataController(view: view), super(BoardState.initial(view.id)) { @@ -57,6 +61,9 @@ class BoardBloc extends Bloc { didReceiveGroups: (List groups) { emit(state.copyWith(groups: groups)); }, + didReceiveRows: (List rowInfos) { + emit(state.copyWith(rowInfos: rowInfos)); + }, ); }, ); @@ -68,7 +75,7 @@ class BoardBloc extends Bloc { return super.close(); } - GridRowCache? getRowCache(String blockId, String rowId) { + GridRowCache? getRowCache(String blockId) { final GridBlockCache? blockCache = _dataController.blocks[blockId]; return blockCache?.rowCache; } @@ -92,6 +99,9 @@ class BoardBloc extends Bloc { boardDataController.addColumns(columns); }, + onRowsChanged: (List rowInfos, RowChangeReason reason) { + add(BoardEvent.didReceiveRows(rowInfos)); + }, onError: (err) { Log.error(err); }, @@ -100,15 +110,15 @@ class BoardBloc extends Bloc { List _buildRows(List rows) { return rows.map((row) { - final rowInfo = RowInfo( - gridId: _dataController.gridId, - blockId: row.blockId, - id: row.id, - fields: _dataController.fieldCache.unmodifiableFields, - height: row.height.toDouble(), - rawRow: row, - ); - return BoardColumnItem(row: rowInfo); + // final rowInfo = RowInfo( + // gridId: _dataController.gridId, + // blockId: row.blockId, + // id: row.id, + // fields: _dataController.fieldCache.unmodifiableFields, + // height: row.height.toDouble(), + // rawRow: row, + // ); + return BoardColumnItem(row: row); }).toList(); } @@ -131,6 +141,8 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.createRow() = _CreateRow; const factory BoardEvent.didReceiveGroups(List groups) = _DidReceiveGroup; + const factory BoardEvent.didReceiveRows(List rowInfos) = + _DidReceiveRows; const factory BoardEvent.didReceiveGridUpdate( GridPB grid, ) = _DidReceiveGridUpdate; @@ -186,7 +198,7 @@ class GridFieldEquatable extends Equatable { } class BoardColumnItem extends AFColumnItem { - final RowInfo row; + final RowPB row; BoardColumnItem({required this.row}); diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index c8e20b6172..4a0333f30b 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -3,6 +3,8 @@ import 'dart:collection'; import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/grid_service.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'dart:async'; @@ -12,6 +14,10 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef OnGroupChanged = void Function(List); +typedef OnRowsChanged = void Function( + List rowInfos, + RowChangeReason, +); typedef OnError = void Function(FlowyError); class BoardDataController { @@ -21,17 +27,25 @@ class BoardDataController { // key: the block id final LinkedHashMap _blocks; - UnmodifiableMapView get blocks => - UnmodifiableMapView(_blocks); + LinkedHashMap get blocks => _blocks; OnFieldsChanged? _onFieldsChanged; OnGridChanged? _onGridChanged; OnGroupChanged? _onGroupChanged; + OnRowsChanged? _onRowsChanged; OnError? _onError; + List get rowInfos { + final List rows = []; + for (var block in _blocks.values) { + rows.addAll(block.rows); + } + return rows; + } + BoardDataController({required ViewPB view}) : gridId = view.id, - _blocks = LinkedHashMap.identity(), + _blocks = LinkedHashMap.new(), _gridFFIService = GridService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); @@ -39,11 +53,13 @@ class BoardDataController { OnGridChanged? onGridChanged, OnFieldsChanged? onFieldsChanged, OnGroupChanged? onGroupChanged, + OnRowsChanged? onRowsChanged, OnError? onError, }) { _onGridChanged = onGridChanged; _onFieldsChanged = onFieldsChanged; _onGroupChanged = onGroupChanged; + _onRowsChanged = onRowsChanged; _onError = onError; fieldCache.addListener(onFields: (fields) { @@ -57,6 +73,7 @@ class BoardDataController { () => result.fold( (grid) async { _onGridChanged?.call(grid); + _initialBlocks(grid.blocks); return await _loadFields(grid).then((result) { return result.fold( (l) { @@ -85,6 +102,29 @@ class BoardDataController { } } + void _initialBlocks(List blocks) { + for (final block in blocks) { + if (_blocks[block.id] != null) { + Log.warn("Initial duplicate block's cache: ${block.id}"); + return; + } + + final cache = GridBlockCache( + gridId: gridId, + block: block, + fieldCache: fieldCache, + ); + + cache.addListener( + onChangeReason: (reason) { + _onRowsChanged?.call(rowInfos, reason); + }, + ); + + _blocks[block.id] = cache; + } + } + Future> _loadFields(GridPB grid) async { final result = await _gridFFIService.getFields(fieldIds: grid.fields); return Future( diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart new file mode 100644 index 0000000000..9ba66c2aab --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -0,0 +1,111 @@ +import 'dart:collection'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_service.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import 'card_data_controller.dart'; + +part 'card_bloc.freezed.dart'; + +class BoardCardBloc extends Bloc { + final RowFFIService _rowService; + final CardDataController _dataController; + + BoardCardBloc({ + required String gridId, + required CardDataController dataController, + }) : _rowService = RowFFIService( + gridId: gridId, + blockId: dataController.rowPB.blockId, + rowId: dataController.rowPB.id, + ), + _dataController = dataController, + super(BoardCardState.initial( + dataController.rowPB, dataController.loadData())) { + on( + (event, emit) async { + await event.map( + initial: (_InitialRow value) async { + await _startListening(); + }, + createRow: (_CreateRow value) { + _rowService.createRow(); + }, + didReceiveCells: (_DidReceiveCells value) async { + final cells = value.gridCellMap.values + .map((e) => GridCellEquatable(e.field)) + .toList(); + emit(state.copyWith( + gridCellMap: value.gridCellMap, + cells: UnmodifiableListView(cells), + changeReason: value.reason, + )); + }, + ); + }, + ); + } + + @override + Future close() async { + _dataController.dispose(); + return super.close(); + } + + Future _startListening() async { + _dataController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(BoardCardEvent.didReceiveCells(cells, reason)); + } + }, + ); + } +} + +@freezed +class BoardCardEvent with _$BoardCardEvent { + const factory BoardCardEvent.initial() = _InitialRow; + const factory BoardCardEvent.createRow() = _CreateRow; + const factory BoardCardEvent.didReceiveCells( + GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells; +} + +@freezed +class BoardCardState with _$BoardCardState { + const factory BoardCardState({ + required RowPB rowPB, + required GridCellMap gridCellMap, + required UnmodifiableListView cells, + RowChangeReason? changeReason, + }) = _BoardCardState; + + factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) => + BoardCardState( + rowPB: rowPB, + gridCellMap: cellDataMap, + cells: UnmodifiableListView( + cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(), + ), + ); +} + +class GridCellEquatable extends Equatable { + final FieldPB _field; + + const GridCellEquatable(FieldPB field) : _field = field; + + @override + List get props => [ + _field.id, + _field.fieldType, + _field.visibility, + _field.width, + ]; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart new file mode 100644 index 0000000000..d9ac41f10b --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart @@ -0,0 +1,49 @@ +import 'package:app_flowy/plugins/board/presentation/card/card_cell_builder.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flutter/foundation.dart'; + +typedef OnCardChanged = void Function(GridCellMap, RowChangeReason); + +class CardDataController extends BoardCellBuilderDelegate { + final RowPB rowPB; + final GridFieldCache _fieldCache; + final GridRowCache _rowCache; + final List _onCardChangedListeners = []; + + CardDataController({ + required this.rowPB, + required GridFieldCache fieldCache, + required GridRowCache rowCache, + }) : _fieldCache = fieldCache, + _rowCache = rowCache; + + GridCellMap loadData() { + return _rowCache.loadGridCells(rowPB.id); + } + + void addListener({OnCardChanged? onRowChanged}) { + _onCardChangedListeners.add(_rowCache.addListener( + rowId: rowPB.id, + onCellUpdated: onRowChanged, + )); + } + + void dispose() { + for (final fn in _onCardChangedListeners) { + _rowCache.removeRowListener(fn); + } + } + + @override + GridCellFieldNotifier buildFieldNotifier() { + return GridCellFieldNotifier( + notifier: GridCellFieldNotifierImpl(_fieldCache)); + } + + @override + GridCellCache get cellCache => _rowCache.cellCache; +} diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 2954a7cbf9..36d181ae3e 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index f8367c1392..1d5114ef05 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,5 +1,6 @@ // ignore_for_file: unused_field +import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; @@ -7,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/board_bloc.dart'; import 'card/card.dart'; +import 'card/card_cell_builder.dart'; class BoardPage extends StatelessWidget { final ViewPB view; @@ -51,6 +53,7 @@ class BoardContent extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), child: AFBoard( + key: UniqueKey(), dataController: context.read().boardDataController, headerBuilder: _buildHeader, footBuilder: _buildFooter, @@ -87,10 +90,29 @@ class BoardContent extends StatelessWidget { } Widget _buildCard(BuildContext context, AFColumnItem item) { - final rowInfo = (item as BoardColumnItem).row; + final rowPB = (item as BoardColumnItem).row; + final rowCache = context.read().getRowCache(rowPB.blockId); + + /// Return placeholder widget if the rowCache is null. + if (rowCache == null) return SizedBox(key: ObjectKey(item)); + + final fieldCache = context.read().fieldCache; + final gridId = context.read().gridId; + final cardController = CardDataController( + fieldCache: fieldCache, + rowCache: rowCache, + rowPB: rowPB, + ); + + final cellBuilder = BoardCellBuilder(cardController); + return AppFlowyColumnItemCard( key: ObjectKey(item), - child: BoardCard(rowInfo: rowInfo), + child: BoardCard( + cellBuilder: cellBuilder, + dataController: cardController, + gridId: gridId, + ), ); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart new file mode 100644 index 0000000000..57920aa631 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -0,0 +1,21 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter/material.dart'; + +class BoardCheckboxCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardCheckboxCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardCheckboxCellState(); +} + +class _BoardCheckboxCellState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart new file mode 100644 index 0000000000..8270c7978b --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -0,0 +1,21 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter/material.dart'; + +class BoardDateCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardDateCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardDateCellState(); +} + +class _BoardDateCellState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart new file mode 100644 index 0000000000..80797bc821 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -0,0 +1,21 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter/material.dart'; + +class BoardNumberCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardNumberCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardNumberCellState(); +} + +class _BoardNumberCellState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart new file mode 100644 index 0000000000..f7f084d6cd --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -0,0 +1,21 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter/material.dart'; + +class BoardUrlCell extends StatefulWidget { + final GridCellControllerBuilder cellControllerBuilder; + + const BoardUrlCell({ + required this.cellControllerBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardUrlCellState(); +} + +class _BoardUrlCellState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 020cc49db3..6c66ec968a 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -1,13 +1,63 @@ -import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; +import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class BoardCard extends StatelessWidget { - final RowInfo rowInfo; +import 'card_cell_builder.dart'; - const BoardCard({required this.rowInfo, Key? key}) : super(key: key); +class BoardCard extends StatefulWidget { + final String gridId; + final CardDataController dataController; + final BoardCellBuilder cellBuilder; + + const BoardCard({ + required this.gridId, + required this.dataController, + required this.cellBuilder, + Key? key, + }) : super(key: key); + + @override + State createState() => _BoardCardState(); +} + +class _BoardCardState extends State { + late BoardCardBloc _cardBloc; + + @override + void initState() { + _cardBloc = BoardCardBloc( + gridId: widget.gridId, + dataController: widget.dataController, + ); + super.initState(); + } @override Widget build(BuildContext context) { - return const SizedBox(height: 20, child: Text('1234')); + return BlocProvider.value( + value: _cardBloc, + child: BlocBuilder( + builder: (context, state) { + return SizedBox( + height: 100, + child: Column( + children: _makeCells(context, state.gridCellMap), + ), + ); + }, + ), + ); + } + + List _makeCells(BuildContext context, GridCellMap cellMap) { + return cellMap.values.map( + (cellId) { + final child = widget.cellBuilder.buildCell(cellId); + + return SizedBox(height: 39, child: child); + }, + ).toList(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart index e69de29bb2..10ae0db680 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_cell_builder.dart @@ -0,0 +1,69 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/material.dart'; + +import 'board_checkbox_cell.dart'; +import 'board_date_cell.dart'; +import 'board_number_cell.dart'; +import 'board_select_option_cell.dart'; +import 'board_text_cell.dart'; +import 'board_url_cell.dart'; + +abstract class BoardCellBuilderDelegate + extends GridCellControllerBuilderDelegate { + GridCellCache get cellCache; +} + +class BoardCellBuilder { + final BoardCellBuilderDelegate delegate; + + BoardCellBuilder(this.delegate); + + Widget buildCell(GridCellIdentifier cellId) { + final cellControllerBuilder = GridCellControllerBuilder( + delegate: delegate, + cellId: cellId, + cellCache: delegate.cellCache, + ); + + final key = cellId.key(); + switch (cellId.fieldType) { + case FieldType.Checkbox: + return BoardCheckboxCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.DateTime: + return BoardDateCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.SingleSelect: + return BoardSelectOptionCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.MultiSelect: + return BoardSelectOptionCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.Number: + return BoardNumberCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.RichText: + return BoardTextCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.URL: + return BoardUrlCell( + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + } + throw UnimplementedError; + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 32488599ea..aae6dc684e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -46,7 +46,7 @@ class GridDataController { GridDataController({required ViewPB view}) : gridId = view.id, - _blocks = LinkedHashMap.identity(), + _blocks = LinkedHashMap.new(), _gridFFIService = GridService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart index 0b4499682f..7e3e9a21bd 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart @@ -12,10 +12,10 @@ part 'row_action_sheet_bloc.freezed.dart'; class RowActionSheetBloc extends Bloc { - final RowService _rowService; + final RowFFIService _rowService; RowActionSheetBloc({required RowInfo rowData}) - : _rowService = RowService( + : _rowService = RowFFIService( gridId: rowData.gridId, blockId: rowData.blockId, rowId: rowData.id, diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index e6a68cd080..372287fd1f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -12,13 +12,13 @@ import 'row_service.dart'; part 'row_bloc.freezed.dart'; class RowBloc extends Bloc { - final RowService _rowService; + final RowFFIService _rowService; final GridRowDataController _dataController; RowBloc({ required RowInfo rowInfo, required GridRowDataController dataController, - }) : _rowService = RowService( + }) : _rowService = RowFFIService( gridId: rowInfo.gridId, blockId: rowInfo.blockId, rowId: rowInfo.id, @@ -35,13 +35,12 @@ class RowBloc extends Bloc { _rowService.createRow(); }, didReceiveCells: (_DidReceiveCells value) async { - final fields = value.gridCellMap.values + final cells = value.gridCellMap.values .map((e) => GridCellEquatable(e.field)) .toList(); - final snapshots = UnmodifiableListView(fields); emit(state.copyWith( gridCellMap: value.gridCellMap, - snapshots: snapshots, + cells: UnmodifiableListView(cells), changeReason: value.reason, )); }, @@ -80,7 +79,7 @@ class RowState with _$RowState { const factory RowState({ required RowInfo rowInfo, required GridCellMap gridCellMap, - required UnmodifiableListView snapshots, + required UnmodifiableListView cells, RowChangeReason? changeReason, }) = _RowState; @@ -88,8 +87,9 @@ class RowState with _$RowState { RowState( rowInfo: rowInfo, gridCellMap: cellDataMap, - snapshots: UnmodifiableListView( - cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()), + cells: UnmodifiableListView( + cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList(), + ), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart index 31a54aa29b..78783fc894 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -13,10 +13,6 @@ class GridRowDataController extends GridCellBuilderDelegate { final GridFieldCache _fieldCache; final GridRowCache _rowCache; - GridFieldCache get fieldCache => _fieldCache; - - GridRowCache get rowCache => _rowCache; - GridRowDataController({ required this.rowInfo, required GridFieldCache fieldCache, @@ -49,5 +45,5 @@ class GridRowDataController extends GridCellBuilderDelegate { } @override - GridCellCache get cellCache => rowCache.cellCache; + GridCellCache get cellCache => _rowCache.cellCache; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index 94e047c1f7..0f056a4006 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -5,12 +5,12 @@ import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; -class RowService { +class RowFFIService { final String gridId; final String blockId; final String rowId; - RowService( + RowFFIService( {required this.gridId, required this.blockId, required this.rowId}); Future> createRow() { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 6c995d57eb..3864c1a6a0 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -164,7 +164,7 @@ class RowContent extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( buildWhen: (previous, current) => - !listEquals(previous.snapshots, current.snapshots), + !listEquals(previous.cells, current.cells), builder: (context, state) { return IntrinsicHeight( child: Row( diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index 3d9d76fe29..dc1e0de3d7 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -53,7 +53,7 @@ class MenuUser extends StatelessWidget { borderRadius: Corners.s5Border, child: CircleAvatar( backgroundColor: Colors.transparent, - child: svgWidget('emoji/$iconUrl'), + child: Container(), )), ); } diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index 5fb0be874c..528ffbcafb 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -150,7 +150,7 @@ impl FolderTest { // assert_eq!(json, expected_json); // } FolderScript::AssertWorkspace(workspace) => { - assert_eq!(self.workspace, workspace); + assert_eq!(self.workspace, workspace, "Workspace not equal"); } FolderScript::ReadWorkspace(workspace_id) => { let workspace = read_workspace(sdk, workspace_id).await.pop().unwrap(); @@ -166,7 +166,7 @@ impl FolderTest { // assert_eq!(json, expected_json); // } FolderScript::AssertApp(app) => { - assert_eq!(self.app, app); + assert_eq!(self.app, app, "App not equal"); } FolderScript::ReadApp(app_id) => { let app = read_app(sdk, &app_id).await; @@ -184,7 +184,7 @@ impl FolderTest { self.view = view; } FolderScript::AssertView(view) => { - assert_eq!(self.view, view); + assert_eq!(self.view, view, "View not equal"); } FolderScript::ReadView(view_id) => { let view = read_view(sdk, &view_id).await; @@ -215,7 +215,7 @@ impl FolderTest { } FolderScript::AssertRevisionState { rev_id, state } => { let record = cache.get(rev_id).await.unwrap(); - assert_eq!(record.state, state); + assert_eq!(record.state, state, "Revision state is not match"); if let RevisionState::Ack = state { // There is a defer action that writes the revisions to disk, so we wait here. // Make sure everything is written. @@ -235,7 +235,7 @@ impl FolderTest { .unwrap_or_else(|| panic!("Expected Next revision is {}, but receive None", rev_id.unwrap())); let mut notify = rev_manager.ack_notify(); let _ = notify.recv().await; - assert_eq!(next_revision.rev_id, rev_id.unwrap()); + assert_eq!(next_revision.rev_id, rev_id.unwrap(), "Revision id not match"); } } } From 9d72e36c1981555b1ff880d2facfdd2336abe520 Mon Sep 17 00:00:00 2001 From: appflowy Date: Fri, 12 Aug 2022 22:58:43 +0800 Subject: [PATCH 106/224] chore: update card ui by adding date,text,selectoption cell --- .../card/board_date_cell_bloc.dart | 85 +++++++++++++++++++ .../card/board_number_cell_bloc.dart | 67 +++++++++++++++ .../board/presentation/board_page.dart | 2 +- .../presentation/card/board_date_cell.dart | 43 +++++++++- .../presentation/card/board_number_cell.dart | 43 +++++++++- .../card/board_select_option_cell.dart | 17 +++- .../presentation/card/board_text_cell.dart | 18 +++- .../plugins/board/presentation/card/card.dart | 9 +- .../cell/cell_service/context_builder.dart | 9 +- .../application/cell/number_cell_bloc.dart | 2 +- .../cell/select_option_cell/extension.dart | 7 +- .../select_option_cell.dart | 43 +++++----- .../cell/select_option_cell/text_field.dart | 6 +- .../presentation/home/menu/menu_user.dart | 2 +- .../src/services/row/row_builder.rs | 29 +++++-- frontend/rust-lib/flowy-grid/src/util.rs | 23 ++++- 16 files changed, 347 insertions(+), 58 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart index e69de29bb2..76267ededb 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart @@ -0,0 +1,85 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +part 'board_date_cell_bloc.freezed.dart'; + +class BoardDateCellBloc extends Bloc { + final GridDateCellController cellController; + void Function()? _onCellChangedFn; + + BoardDateCellBloc({required this.cellController}) + : super(BoardDateCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () => _startListening(), + didReceiveCellUpdate: (DateCellDataPB? cellData) { + emit(state.copyWith( + data: cellData, dateStr: _dateStrFromCellData(cellData))); + }, + didReceiveFieldUpdate: (FieldPB value) => + emit(state.copyWith(field: value)), + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((data) { + if (!isClosed) { + add(BoardDateCellEvent.didReceiveCellUpdate(data)); + } + }), + ); + } +} + +@freezed +class BoardDateCellEvent with _$BoardDateCellEvent { + const factory BoardDateCellEvent.initial() = _InitialCell; + const factory BoardDateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = + _DidReceiveCellUpdate; + const factory BoardDateCellEvent.didReceiveFieldUpdate(FieldPB field) = + _DidReceiveFieldUpdate; +} + +@freezed +class BoardDateCellState with _$BoardDateCellState { + const factory BoardDateCellState({ + required DateCellDataPB? data, + required String dateStr, + required FieldPB field, + }) = _BoardDateCellState; + + factory BoardDateCellState.initial(GridDateCellController context) { + final cellData = context.getCellData(); + + return BoardDateCellState( + field: context.field, + data: cellData, + dateStr: _dateStrFromCellData(cellData), + ); + } +} + +String _dateStrFromCellData(DateCellDataPB? cellData) { + String dateStr = ""; + if (cellData != null) { + dateStr = cellData.date + " " + cellData.time; + } + return dateStr; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart new file mode 100644 index 0000000000..2cc4882357 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_number_cell_bloc.dart @@ -0,0 +1,67 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_number_cell_bloc.freezed.dart'; + +class BoardNumberCellBloc + extends Bloc { + final GridNumberCellController cellController; + void Function()? _onCellChangedFn; + BoardNumberCellBloc({ + required this.cellController, + }) : super(BoardNumberCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (content) { + emit(state.copyWith(content: content)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(BoardNumberCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class BoardNumberCellEvent with _$BoardNumberCellEvent { + const factory BoardNumberCellEvent.initial() = _InitialCell; + const factory BoardNumberCellEvent.didReceiveCellUpdate(String cellContent) = + _DidReceiveCellUpdate; +} + +@freezed +class BoardNumberCellState with _$BoardNumberCellState { + const factory BoardNumberCellState({ + required String content, + }) = _BoardNumberCellState; + + factory BoardNumberCellState.initial(GridCellController context) => + BoardNumberCellState( + content: context.getCellData() ?? "", + ); +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 1d5114ef05..b373f65604 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -51,7 +51,7 @@ class BoardContent extends StatelessWidget { return Container( color: Colors.white, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( key: UniqueKey(), dataController: context.read().boardDataController, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index 8270c7978b..f49df90107 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -1,5 +1,8 @@ +import 'package:app_flowy/plugins/board/application/card/board_date_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class BoardDateCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -14,8 +17,46 @@ class BoardDateCell extends StatefulWidget { } class _BoardDateCellState extends State { + late BoardDateCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridDateCellController; + + _cellBloc = BoardDateCellBloc(cellController: cellController) + ..add(const BoardDateCellEvent.initial()); + super.initState(); + } + @override Widget build(BuildContext context) { - return Container(); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + if (state.dateStr.isEmpty) { + return const SizedBox(); + } else { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.dateStr, + fontSize: 14, + ), + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index 80797bc821..ae2f92b231 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -1,5 +1,8 @@ +import 'package:app_flowy/plugins/board/application/card/board_number_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class BoardNumberCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -14,8 +17,46 @@ class BoardNumberCell extends StatefulWidget { } class _BoardNumberCellState extends State { + late BoardNumberCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridNumberCellController; + + _cellBloc = BoardNumberCellBloc(cellController: cellController) + ..add(const BoardNumberCellEvent.initial()); + super.initState(); + } + @override Widget build(BuildContext context) { - return Container(); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.content, + fontSize: 14, + ), + ), + ); + } + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 64a7a124c6..1439045375 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -1,6 +1,6 @@ import 'package:app_flowy/plugins/board/application/card/board_select_option_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; -import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -34,9 +34,18 @@ class _BoardSelectOptionCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - return SelectOptionWrap( - selectOptions: state.selectedOptions, - cellControllerBuilder: widget.cellControllerBuilder, + final children = state.selectedOptions + .map((option) => SelectOptionTag.fromOption( + context: context, + option: option, + )) + .toList(); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.centerLeft, + child: Wrap(children: children, spacing: 4, runSpacing: 2), + ), ); }, ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index fd3b89a9ae..f9425f7f6d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -32,10 +32,20 @@ class _BoardTextCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - return SizedBox( - height: 30, - child: FlowyText.medium(state.content), - ); + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.content, + fontSize: 14, + ), + ), + ); + } }, ), ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 6c66ec968a..f123f21bb9 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -40,11 +40,8 @@ class _BoardCardState extends State { value: _cardBloc, child: BlocBuilder( builder: (context, state) { - return SizedBox( - height: 100, - child: Column( - children: _makeCells(context, state.gridCellMap), - ), + return Column( + children: _makeCells(context, state.gridCellMap), ); }, ), @@ -56,7 +53,7 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(cellId); - return SizedBox(height: 39, child: child); + return child; }, ).toList(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index b7c68a7937..7c2377fdcd 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -1,6 +1,7 @@ part of 'cell_service.dart'; typedef GridCellController = IGridCellController; +typedef GridNumberCellController = IGridCellController; typedef GridSelectOptionCellController = IGridCellController; typedef GridDateCellController @@ -58,7 +59,7 @@ class GridCellControllerBuilder { parser: StringCellDataParser(), reloadOnFieldChanged: true, ); - return GridCellController( + return GridNumberCellController( cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, @@ -127,7 +128,7 @@ class IGridCellController extends Equatable { final GridCellDataLoader _cellDataLoader; final IGridCellDataPersistence _cellDataPersistence; - late final CellListener _cellListener; + CellListener? _cellListener; ValueNotifier? _cellDataNotifier; bool isListening = false; @@ -186,7 +187,7 @@ class IGridCellController extends Equatable { /// For example: /// user input: 12 /// cell display: $12 - _cellListener.start(onCellChanged: (result) { + _cellListener?.start(onCellChanged: (result) { result.fold( (_) => _loadData(), (err) => Log.error(err), @@ -289,7 +290,7 @@ class IGridCellController extends Equatable { return; } _isDispose = true; - _cellListener.stop(); + _cellListener?.stop(); _loadDataOperation?.cancel(); _saveDataOperation?.cancel(); _cellDataNotifier = null; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart index 88b28ea414..2ca989289f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/number_cell_bloc.dart @@ -8,7 +8,7 @@ import 'cell_service/cell_service.dart'; part 'number_cell_bloc.freezed.dart'; class NumberCellBloc extends Bloc { - final GridCellController cellController; + final GridNumberCellController cellController; void Function()? _onCellChangedFn; NumberCellBloc({ diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart index 6946993bae..6fdd8bf6f8 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart @@ -73,7 +73,7 @@ class SelectOptionTag extends StatelessWidget { Key? key, }) : super(key: key); - factory SelectOptionTag.fromSelectOption({ + factory SelectOptionTag.fromOption({ required BuildContext context, required SelectOptionPB option, VoidCallback? onSelected, @@ -91,7 +91,8 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { return ChoiceChip( pressElevation: 1, - label: FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), + label: + FlowyText.medium(name, fontSize: 12, overflow: TextOverflow.ellipsis), selectedColor: color, backgroundColor: color, labelPadding: const EdgeInsets.symmetric(horizontal: 6), @@ -133,7 +134,7 @@ class SelectOptionTagCell extends StatelessWidget { Flexible( fit: FlexFit.loose, flex: 2, - child: SelectOptionTag.fromSelectOption( + child: SelectOptionTag.fromOption( context: context, option: option, onSelected: () => onSelected(option), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart index 1bd54e7514..a8d3993a2f 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_cell.dart @@ -153,21 +153,25 @@ class SelectOptionWrap extends StatelessWidget { if (selectOptions.isEmpty && cellStyle != null) { child = Align( alignment: Alignment.centerLeft, - child: FlowyText.medium(cellStyle!.placeholder, - fontSize: 14, color: theme.shader3), + child: FlowyText.medium( + cellStyle!.placeholder, + fontSize: 14, + color: theme.shader3, + ), ); } else { - final tags = selectOptions - .map( - (option) => SelectOptionTag.fromSelectOption( - context: context, - option: option, - ), - ) - .toList(); child = Align( alignment: Alignment.centerLeft, - child: Wrap(children: tags, spacing: 4, runSpacing: 2), + child: Wrap( + children: selectOptions + .map((option) => SelectOptionTag.fromOption( + context: context, + option: option, + )) + .toList(), + spacing: 4, + runSpacing: 2, + ), ); } @@ -176,15 +180,14 @@ class SelectOptionWrap extends StatelessWidget { fit: StackFit.expand, children: [ child, - InkWell( - onTap: () { - onFocus?.call(true); - final cellContext = - cellControllerBuilder.build() as GridSelectOptionCellController; - SelectOptionCellEditor.show( - context, cellContext, () => onFocus?.call(false)); - }, - ), + InkWell(onTap: () { + onFocus?.call(true); + SelectOptionCellEditor.show( + context, + cellControllerBuilder.build() as GridSelectOptionCellController, + () => onFocus?.call(false), + ); + }), ], ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart index 10b04cfb58..5482a403cc 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart @@ -49,7 +49,8 @@ class SelectOptionTextField extends StatelessWidget { initialTags: selectedOptionMap.keys.toList(), focusNode: _focusNode, textSeparators: const [' ', ','], - inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) { + inputfieldBuilder: (BuildContext context, editController, focusNode, + error, onChanged, onSubmitted) { return ((context, sc, tags, onTagDelegate) { return TextField( autofocus: true, @@ -99,7 +100,8 @@ class SelectOptionTextField extends StatelessWidget { } final children = selectedOptionMap.values - .map((option) => SelectOptionTag.fromSelectOption(context: context, option: option)) + .map((option) => + SelectOptionTag.fromOption(context: context, option: option)) .toList(); return Padding( padding: const EdgeInsets.all(8.0), diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index dc1e0de3d7..3d9d76fe29 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -53,7 +53,7 @@ class MenuUser extends StatelessWidget { borderRadius: Corners.s5Border, child: CircleAvatar( backgroundColor: Colors.transparent, - child: Container(), + child: svgWidget('emoji/$iconUrl'), )), ); } diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index ad294e74dc..3db4c0b550 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -1,6 +1,5 @@ use crate::services::cell::apply_cell_data_changeset; -use crate::services::field::SelectOptionCellChangeset; -use flowy_error::{FlowyError, FlowyResult}; +use crate::services::field::{DateCellChangesetPB, SelectOptionCellChangeset}; use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; use std::collections::HashMap; @@ -35,17 +34,33 @@ impl<'a> RowRevisionBuilder<'a> { } } - pub fn insert_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_cell(&mut self, field_id: &str, data: String) { match self.field_rev_map.get(&field_id.to_owned()) { None => { - let msg = format!("Can't find the field with id: {}", field_id); - Err(FlowyError::internal().context(msg)) + tracing::warn!("Can't find the field with id: {}", field_id); } Some(field_rev) => { - let data = apply_cell_data_changeset(data, None, field_rev)?; + let data = apply_cell_data_changeset(data, None, field_rev).unwrap(); + let cell = CellRevision::new(data); + self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); + } + } + } + + pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) { + match self.field_rev_map.get(&field_id.to_owned()) { + None => { + tracing::warn!("Invalid field_id: {}", field_id); + } + Some(field_rev) => { + let cell_data = serde_json::to_string(&DateCellChangesetPB { + date: Some(timestamp.to_string()), + time: None, + }) + .unwrap(); + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); let cell = CellRevision::new(data); self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); - Ok(()) } } } diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 7897dbfdaa..5d763d2bde 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -3,6 +3,7 @@ use crate::services::field::*; use crate::services::row::RowRevisionBuilder; use flowy_grid_data_model::revision::BuildGridContext; use flowy_sync::client_grid::GridBuilder; +use lib_infra::util::timestamp; pub fn make_default_grid() -> BuildGridContext { let mut grid_builder = GridBuilder::new(); @@ -40,24 +41,40 @@ pub fn make_default_board() -> BuildGridContext { .visibility(true) .primary(true) .build(); + let text_field_id = text_field.id.clone(); grid_builder.add_field(text_field); + // date + let date_type_option = DateTypeOptionBuilder::default(); + let date_field = FieldBuilder::new(date_type_option) + .name("Date") + .visibility(true) + .build(); + let date_field_id = date_field.id.clone(); + let timestamp = timestamp(); + grid_builder.add_field(date_field); + // single select let in_progress_option = SelectOptionPB::new("In progress"); let not_started_option = SelectOptionPB::new("Not started"); let done_option = SelectOptionPB::new("Done"); - let single_select = SingleSelectTypeOptionBuilder::default() + let single_select_type_option = SingleSelectTypeOptionBuilder::default() .add_option(not_started_option.clone()) .add_option(in_progress_option) .add_option(done_option); - let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); + let single_select_field = FieldBuilder::new(single_select_type_option) + .name("Status") + .visibility(true) + .build(); let single_select_field_id = single_select_field.id.clone(); grid_builder.add_field(single_select_field); // Insert rows - for _ in 0..3 { + for i in 0..10 { let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); row_builder.insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()); + row_builder.insert_cell(&text_field_id, format!("Card {}", i)); + row_builder.insert_date_cell(&date_field_id, timestamp); let row = row_builder.build(); grid_builder.add_row(row); } From 022997e6ff571f7c2a2786c11e9e41e120105e6c Mon Sep 17 00:00:00 2001 From: Nikunj Date: Fri, 12 Aug 2022 22:10:15 +0530 Subject: [PATCH 107/224] fix: fixed hover color #685 --- .../plugins/grid/presentation/widgets/footer/grid_footer.dart | 2 +- .../plugins/grid/presentation/widgets/header/grid_header.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart index f7b5ab350a..fba4215891 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/footer/grid_footer.dart @@ -14,7 +14,7 @@ class GridAddRowButton extends StatelessWidget { final theme = context.watch(); return FlowyButton( text: const FlowyText.medium('New row', fontSize: 12), - hoverColor: theme.hover, + hoverColor: theme.shader6, onTap: () => context.read().add(const GridEvent.createRow()), leftIcon: svgWidget("home/add"), ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart index 1a8ed0be20..9d33768e65 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/grid_header.dart @@ -156,7 +156,7 @@ class CreateFieldButton extends StatelessWidget { return FlowyButton( text: const FlowyText.medium('New column', fontSize: 12), - hoverColor: theme.hover, + hoverColor: theme.shader6, onTap: () => FieldEditor( gridId: gridId, fieldName: "", From 2282aa948e612bf4c0100f13900a02026fdcafaa Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 10:01:40 +0800 Subject: [PATCH 108/224] chore: support link/number/multiselect/checkbox board cell --- .../card/board_checkbox_cell.dart | 40 +++++++++++++- .../presentation/card/board_date_cell.dart | 13 ++--- .../presentation/card/board_number_cell.dart | 13 ++--- .../card/board_select_option_cell.dart | 9 ++-- .../presentation/card/board_text_cell.dart | 13 ++--- .../presentation/card/board_url_cell.dart | 45 +++++++++++++++- .../plugins/board/presentation/card/card.dart | 12 +++-- .../cell/cell_service/context_builder.dart | 1 + .../application/cell/checkbox_cell_bloc.dart | 2 +- .../widgets/cell/checkbox_cell.dart | 3 +- .../widgets/cell/url_cell/url_cell.dart | 4 +- .../appflowy_board/lib/src/widgets/board.dart | 2 +- .../board_column/board_column_data.dart | 17 +++--- .../lib/src/widgets/board_data.dart | 10 ++-- .../reorder_phantom/phantom_controller.dart | 2 +- .../multi_select_type_option.rs | 8 +-- .../text_type_option/text_type_option.rs | 4 +- frontend/rust-lib/flowy-grid/src/util.rs | 54 +++++++++++++++++++ .../flowy-grid/tests/grid/grid_editor.rs | 6 +-- 19 files changed, 195 insertions(+), 63 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart index 57920aa631..c816964d3c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_checkbox_cell.dart @@ -1,5 +1,9 @@ +import 'package:app_flowy/plugins/board/application/card/board_checkbox_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class BoardCheckboxCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -14,8 +18,42 @@ class BoardCheckboxCell extends StatefulWidget { } class _BoardCheckboxCellState extends State { + late BoardCheckboxCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridCheckboxCellController; + _cellBloc = BoardCheckboxCellBloc(cellController: cellController); + _cellBloc.add(const BoardCheckboxCellEvent.initial()); + super.initState(); + } + @override Widget build(BuildContext context) { - return Container(); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + final icon = state.isSelected + ? svgWidget('editor/editor_check') + : svgWidget('editor/editor_uncheck'); + return Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + iconPadding: EdgeInsets.zero, + icon: icon, + width: 20, + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart index f49df90107..4a52d82116 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_date_cell.dart @@ -38,14 +38,11 @@ class _BoardDateCellState extends State { if (state.dateStr.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.dateStr, - fontSize: 14, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.dateStr, + fontSize: 14, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart index ae2f92b231..096592583e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_number_cell.dart @@ -38,14 +38,11 @@ class _BoardNumberCellState extends State { if (state.content.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.content, - fontSize: 14, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.content, + fontSize: 14, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index 1439045375..d430f869c1 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -40,12 +40,9 @@ class _BoardSelectOptionCellState extends State { option: option, )) .toList(); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.centerLeft, - child: Wrap(children: children, spacing: 4, runSpacing: 2), - ), + return Align( + alignment: Alignment.centerLeft, + child: Wrap(children: children, spacing: 4, runSpacing: 2), ); }, ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index f9425f7f6d..8cb5c4987e 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -35,14 +35,11 @@ class _BoardTextCellState extends State { if (state.content.isEmpty) { return const SizedBox(); } else { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.content, - fontSize: 14, - ), + return Align( + alignment: Alignment.centerLeft, + child: FlowyText.regular( + state.content, + fontSize: 14, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index f7f084d6cd..5493b0d45b 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -1,5 +1,8 @@ +import 'package:app_flowy/plugins/board/application/card/board_url_cell_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class BoardUrlCell extends StatefulWidget { final GridCellControllerBuilder cellControllerBuilder; @@ -14,8 +17,48 @@ class BoardUrlCell extends StatefulWidget { } class _BoardUrlCellState extends State { + late BoardURLCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as GridURLCellController; + _cellBloc = BoardURLCellBloc(cellController: cellController); + _cellBloc.add(const BoardURLCellEvent.initial()); + super.initState(); + } + @override Widget build(BuildContext context) { - return Container(); + final theme = context.watch(); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + final richText = RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + ); + + return Align( + alignment: Alignment.centerLeft, + child: richText, + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index f123f21bb9..344c72a767 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -40,8 +40,11 @@ class _BoardCardState extends State { value: _cardBloc, child: BlocBuilder( builder: (context, state) { - return Column( - children: _makeCells(context, state.gridCellMap), + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: _makeCells(context, state.gridCellMap), + ), ); }, ), @@ -53,7 +56,10 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(cellId); - return child; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: child, + ); }, ).toList(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index 7c2377fdcd..c8e92809d7 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -1,6 +1,7 @@ part of 'cell_service.dart'; typedef GridCellController = IGridCellController; +typedef GridCheckboxCellController = IGridCellController; typedef GridNumberCellController = IGridCellController; typedef GridSelectOptionCellController = IGridCellController; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart index 44b94a3ddf..f5e7a451e2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/checkbox_cell_bloc.dart @@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart'; part 'checkbox_cell_bloc.freezed.dart'; class CheckboxCellBloc extends Bloc { - final GridCellController cellController; + final GridCheckboxCellController cellController; void Function()? _onCellChangedFn; CheckboxCellBloc({ diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart index ac80303f0d..adffa3ef16 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checkbox_cell.dart @@ -22,7 +22,8 @@ class _CheckboxCellState extends GridCellState { @override void initState() { - final cellController = widget.cellControllerBuilder.build(); + final cellController = + widget.cellControllerBuilder.build() as GridCheckboxCellController; _cellBloc = getIt(param1: cellController) ..add(const CheckboxCellEvent.initial()); super.initState(); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart index 585ef1ce85..102595e166 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/url_cell/url_cell.dart @@ -90,9 +90,9 @@ class _GridURLCellState extends GridCellState { @override void initState() { - final cellContext = + final cellController = widget.cellControllerBuilder.build() as GridURLCellController; - _cellBloc = URLCellBloc(cellController: cellContext); + _cellBloc = URLCellBloc(cellController: cellController); _cellBloc.add(const URLCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 03d9ca1750..09f1590f3d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -202,7 +202,7 @@ class _BoardContentState extends State { return ChangeNotifierProvider.value( key: ValueKey(columnData.id), value: widget.dataController.columnController(columnData.id), - child: Consumer( + child: Consumer( builder: (context, value, child) { return ConstrainedBox( constraints: widget.columnConstraints, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index cfce93af00..847f07c86e 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -12,7 +12,7 @@ abstract class AFColumnItem extends ReoderFlexItem { String toString() => id; } -/// [BoardColumnDataController] is used to handle the [AFBoardColumnData]. +/// [AFBoardColumnDataController] is used to handle the [AFBoardColumnData]. /// * Remove an item by calling [removeAt] method. /// * Move item to another position by calling [move] method. /// * Insert item to index by calling [insert] method @@ -20,10 +20,10 @@ abstract class AFColumnItem extends ReoderFlexItem { /// /// All there operations will notify listeners by default. /// -class BoardColumnDataController extends ChangeNotifier with EquatableMixin { +class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { final AFBoardColumnData columnData; - BoardColumnDataController({ + AFBoardColumnDataController({ required this.columnData, }); @@ -42,7 +42,8 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { AFColumnItem removeAt(int index, {bool notify = true}) { assert(index >= 0); - Log.debug('[$BoardColumnDataController] $columnData remove item at $index'); + Log.debug( + '[$AFBoardColumnDataController] $columnData remove item at $index'); final item = columnData._items.removeAt(index); if (notify) { notifyListeners(); @@ -64,7 +65,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { return false; } Log.debug( - '[$BoardColumnDataController] $columnData move item from $fromIndex to $toIndex'); + '[$AFBoardColumnDataController] $columnData move item from $fromIndex to $toIndex'); final item = columnData._items.removeAt(fromIndex); columnData._items.insert(toIndex, item); notifyListeners(); @@ -78,7 +79,7 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { bool insert(int index, AFColumnItem item, {bool notify = true}) { assert(index >= 0); Log.debug( - '[$BoardColumnDataController] $columnData insert $item at $index'); + '[$AFBoardColumnDataController] $columnData insert $item at $index'); if (columnData._items.length > index) { columnData._items.insert(index, item); @@ -100,12 +101,12 @@ class BoardColumnDataController extends ChangeNotifier with EquatableMixin { void replace(int index, AFColumnItem newItem) { if (columnData._items.isEmpty) { columnData._items.add(newItem); - Log.debug('[$BoardColumnDataController] $columnData add $newItem'); + Log.debug('[$AFBoardColumnDataController] $columnData add $newItem'); } else { final removedItem = columnData._items.removeAt(index); columnData._items.insert(index, newItem); Log.debug( - '[$BoardColumnDataController] $columnData replace $removedItem with $newItem at $index'); + '[$AFBoardColumnDataController] $columnData replace $removedItem with $newItem at $index'); } notifyListeners(); diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 00c84daa3c..6e3f45fc7d 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -35,7 +35,7 @@ class AFBoardDataController extends ChangeNotifier List get columnIds => _columnDatas.map((columnData) => columnData.id).toList(); - final LinkedHashMap _columnControllers = + final LinkedHashMap _columnControllers = LinkedHashMap(); AFBoardDataController({ @@ -47,7 +47,7 @@ class AFBoardDataController extends ChangeNotifier void addColumn(AFBoardColumnData columnData, {bool notify = true}) { if (_columnControllers[columnData.id] != null) return; - final controller = BoardColumnDataController(columnData: columnData); + final controller = AFBoardColumnDataController(columnData: columnData); _columnDatas.add(columnData); _columnControllers[columnData.id] = controller; if (notify) notifyListeners(); @@ -84,11 +84,11 @@ class AFBoardDataController extends ChangeNotifier if (columnIds.isNotEmpty && notify) notifyListeners(); } - BoardColumnDataController columnController(String columnId) { + AFBoardColumnDataController columnController(String columnId) { return _columnControllers[columnId]!; } - BoardColumnDataController? getColumnController(String columnId) { + AFBoardColumnDataController? getColumnController(String columnId) { final columnController = _columnControllers[columnId]; if (columnController == null) { Log.warn('Column:[$columnId] \'s controller is not exist'); @@ -153,7 +153,7 @@ class AFBoardDataController extends ChangeNotifier } @override - BoardColumnDataController? controller(String columnId) { + AFBoardColumnDataController? controller(String columnId) { return _columnControllers[columnId]; } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index bd770fa820..1ab7b2da23 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -7,7 +7,7 @@ import '../reorder_flex/drag_target_inteceptor.dart'; import 'phantom_state.dart'; abstract class BoardPhantomControllerDelegate { - BoardColumnDataController? controller(String columnId); + AFBoardColumnDataController? controller(String columnId); bool removePhantom(String columnId); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 0bffef4c52..be83975c91 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -97,7 +97,7 @@ pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOptionPB); impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder); impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOptionPB); impl MultiSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOptionPB) -> Self { + pub fn add_option(mut self, opt: SelectOptionPB) -> Self { self.0.options.push(opt); self } @@ -127,9 +127,9 @@ mod tests { let facebook_option = SelectOptionPB::new("Facebook"); let twitter_option = SelectOptionPB::new("Twitter"); let multi_select = MultiSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()) - .option(twitter_option); + .add_option(google_option.clone()) + .add_option(facebook_option.clone()) + .add_option(twitter_option); let field_rev = FieldBuilder::new(multi_select) .name("Platform") diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index f21ca91f8c..6c50ce8da3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -156,8 +156,8 @@ mod tests { let ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR); let cell_data_changeset = SelectOptionCellChangeset::from_insert(&ids).to_str(); let multi_select = MultiSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()); + .add_option(google_option.clone()) + .add_option(facebook_option.clone()); let multi_select_field_rev = FieldBuilder::new(multi_select).build(); let multi_type_option = MultiSelectTypeOptionPB::from(&multi_select_field_rev); let cell_data = multi_type_option diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 5d763d2bde..ca1d92a54c 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -69,12 +69,66 @@ pub fn make_default_board() -> BuildGridContext { let single_select_field_id = single_select_field.id.clone(); grid_builder.add_field(single_select_field); + // MultiSelect + let apple_option = SelectOptionPB::new("Apple"); + let banana_option = SelectOptionPB::new("Banana"); + let pear_option = SelectOptionPB::new("Pear"); + let multi_select_type_option = MultiSelectTypeOptionBuilder::default() + .add_option(banana_option.clone()) + .add_option(apple_option.clone()) + .add_option(pear_option.clone()); + let multi_select_field = FieldBuilder::new(multi_select_type_option) + .name("Fruit") + .visibility(true) + .build(); + let multi_select_field_id = multi_select_field.id.clone(); + grid_builder.add_field(multi_select_field); + + // Number + let number_type_option = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD); + let number_field = FieldBuilder::new(number_type_option) + .name("Price") + .visibility(true) + .build(); + let number_field_id = number_field.id.clone(); + grid_builder.add_field(number_field); + + // Checkbox + let checkbox_type_option = CheckboxTypeOptionBuilder::default(); + let checkbox_field = FieldBuilder::new(checkbox_type_option).name("Reimbursement").build(); + let checkbox_field_id = checkbox_field.id.clone(); + grid_builder.add_field(checkbox_field); + + // Url + let url_type_option = URLTypeOptionBuilder::default(); + let url_field = FieldBuilder::new(url_type_option).name("Shop Link").build(); + let url_field_id = url_field.id.clone(); + grid_builder.add_field(url_field); + // Insert rows for i in 0..10 { + // insert single select let mut row_builder = RowRevisionBuilder::new(grid_builder.block_id(), grid_builder.field_revs()); row_builder.insert_select_option_cell(&single_select_field_id, not_started_option.id.clone()); + // insert multi select + row_builder.insert_select_option_cell(&multi_select_field_id, apple_option.id.clone()); + row_builder.insert_select_option_cell(&multi_select_field_id, banana_option.id.clone()); + // insert text row_builder.insert_cell(&text_field_id, format!("Card {}", i)); + // insert date row_builder.insert_date_cell(&date_field_id, timestamp); + // number + row_builder.insert_cell(&number_field_id, format!("{}", i)); + // checkbox + let is_check = if i % 2 == 0 { + CHECK.to_string() + } else { + UNCHECK.to_string() + }; + row_builder.insert_cell(&checkbox_field_id, is_check); + // url + row_builder.insert_cell(&url_field_id, "https://appflowy.io".to_string()); + let row = row_builder.build(); grid_builder.add_row(row); } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index a0f1b6d9cf..2528ce2b32 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -147,9 +147,9 @@ fn make_test_grid() -> BuildGridContext { FieldType::MultiSelect => { // MultiSelect let multi_select = MultiSelectTypeOptionBuilder::default() - .option(SelectOptionPB::new(GOOGLE)) - .option(SelectOptionPB::new(FACEBOOK)) - .option(SelectOptionPB::new(TWITTER)); + .add_option(SelectOptionPB::new(GOOGLE)) + .add_option(SelectOptionPB::new(FACEBOOK)) + .add_option(SelectOptionPB::new(TWITTER)); let multi_select_field = FieldBuilder::new(multi_select) .name("Platform") .visibility(true) From 28e77ae68c0f6593c78d9acb2bd0160f9e1c2bf6 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 11:51:26 +0800 Subject: [PATCH 109/224] chore: add card container --- .../plugins/board/application/board_bloc.dart | 6 +- .../card/board_checkbox_cell_bloc.dart | 71 ++++++++++ .../application/card/board_url_cell_bloc.dart | 78 ++++++++++ .../plugins/board/presentation/card/card.dart | 24 +++- .../presentation/card/card_container.dart | 133 ++++++++++++++++++ .../widgets/cell/cell_accessory.dart | 14 +- .../widgets/cell/cell_builder.dart | 12 ++ .../widgets/cell/cell_container.dart | 36 ++--- .../presentation/widgets/row/grid_row.dart | 31 ++-- .../example/lib/multi_board_list_example.dart | 15 +- 10 files changed, 364 insertions(+), 56 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart create mode 100644 frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 37487a4eec..08b519c1a3 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -108,8 +108,8 @@ class BoardBloc extends Bloc { ); } - List _buildRows(List rows) { - return rows.map((row) { + List _buildRows(List rows) { + final items = rows.map((row) { // final rowInfo = RowInfo( // gridId: _dataController.gridId, // blockId: row.blockId, @@ -120,6 +120,8 @@ class BoardBloc extends Bloc { // ); return BoardColumnItem(row: row); }).toList(); + + return [...items]; } Future _loadGrid(Emitter emit) async { diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart new file mode 100644 index 0000000000..3834db112c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_checkbox_cell_bloc.dart @@ -0,0 +1,71 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_checkbox_cell_bloc.freezed.dart'; + +class BoardCheckboxCellBloc + extends Bloc { + final GridCheckboxCellController cellController; + void Function()? _onCellChangedFn; + BoardCheckboxCellBloc({ + required this.cellController, + }) : super(BoardCheckboxCellState.initial(cellController)) { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(isSelected: _isSelected(cellData))); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellContent) { + if (!isClosed) { + add(BoardCheckboxCellEvent.didReceiveCellUpdate(cellContent ?? "")); + } + }), + ); + } +} + +@freezed +class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent { + const factory BoardCheckboxCellEvent.initial() = _InitialCell; + const factory BoardCheckboxCellEvent.didReceiveCellUpdate( + String cellContent) = _DidReceiveCellUpdate; +} + +@freezed +class BoardCheckboxCellState with _$BoardCheckboxCellState { + const factory BoardCheckboxCellState({ + required bool isSelected, + }) = _CheckboxCellState; + + factory BoardCheckboxCellState.initial(GridCellController context) { + return BoardCheckboxCellState( + isSelected: _isSelected(context.getCellData())); + } +} + +bool _isSelected(String? cellData) { + return cellData == "Yes"; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart new file mode 100644 index 0000000000..045a1633fa --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_url_cell_bloc.dart @@ -0,0 +1,78 @@ +import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'board_url_cell_bloc.freezed.dart'; + +class BoardURLCellBloc extends Bloc { + final GridURLCellController cellController; + void Function()? _onCellChangedFn; + BoardURLCellBloc({ + required this.cellController, + }) : super(BoardURLCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + )); + }, + updateURL: (String url) { + cellController.saveCellData(url, deduplicate: true); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(BoardURLCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class BoardURLCellEvent with _$BoardURLCellEvent { + const factory BoardURLCellEvent.initial() = _InitialCell; + const factory BoardURLCellEvent.updateURL(String url) = _UpdateURL; + const factory BoardURLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = + _DidReceiveCellUpdate; +} + +@freezed +class BoardURLCellState with _$BoardURLCellState { + const factory BoardURLCellState({ + required String content, + required String url, + }) = _BoardURLCellState; + + factory BoardURLCellState.initial(GridURLCellController context) { + final cellData = context.getCellData(); + return BoardURLCellState( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 344c72a767..2c17d5e3c8 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -1,10 +1,12 @@ import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'card_cell_builder.dart'; +import 'card_container.dart'; class BoardCard extends StatefulWidget { final String gridId; @@ -40,8 +42,10 @@ class _BoardCardState extends State { value: _cardBloc, child: BlocBuilder( builder: (context, state) { - return Padding( - padding: const EdgeInsets.all(8.0), + return BoardCardContainer( + accessoryBuilder: (context) { + return [const _CardMoreOption()]; + }, child: Column( children: _makeCells(context, state.gridCellMap), ), @@ -64,3 +68,17 @@ class _BoardCardState extends State { ).toList(); } } + +class _CardMoreOption extends StatelessWidget with CardAccessory { + const _CardMoreOption({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return svgWidget('home/details', color: context.read().iconColor); + } + + @override + void onTap(BuildContext context) { + print('show options'); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart new file mode 100644 index 0000000000..13f3af2195 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -0,0 +1,133 @@ +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class BoardCardContainer extends StatelessWidget { + final Widget child; + final CardAccessoryBuilder? accessoryBuilder; + const BoardCardContainer({ + required this.child, + this.accessoryBuilder, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => _CardContainerNotifier(), + child: Consumer<_CardContainerNotifier>( + builder: (context, notifier, _) { + Widget container = Center(child: child); + if (accessoryBuilder != null) { + final accessories = accessoryBuilder!(context); + if (accessories.isNotEmpty) { + container = _CardEnterRegion( + child: container, + accessories: accessories, + ); + } + } + return Padding( + padding: const EdgeInsets.all(8), + child: container, + ); + }, + ), + ); + } +} + +abstract class CardAccessory implements Widget { + void onTap(BuildContext context); +} + +typedef CardAccessoryBuilder = List Function( + BuildContext buildContext, +); + +class CardAccessoryContainer extends StatelessWidget { + final List accessories; + const CardAccessoryContainer({required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.read(); + final children = accessories.map((accessory) { + final hover = FlowyHover( + style: HoverStyle( + hoverColor: theme.hover, + backgroundColor: theme.surface, + ), + builder: (_, onHover) => Container( + width: 26, + height: 26, + padding: const EdgeInsets.all(3), + child: accessory, + ), + ); + return GestureDetector( + child: hover, + behavior: HitTestBehavior.opaque, + onTap: () => accessory.onTap(context), + ); + }).toList(); + + return Wrap(children: children, spacing: 6); + } +} + +class _CardEnterRegion extends StatelessWidget { + final Widget child; + final List accessories; + const _CardEnterRegion( + {required this.child, required this.accessories, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector<_CardContainerNotifier, bool>( + selector: (context, notifier) => notifier.onEnter, + builder: (context, onEnter, _) { + List children = [child]; + if (onEnter) { + children.add(CardAccessoryContainer(accessories: accessories) + .positioned(right: 0)); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of<_CardContainerNotifier>(context, listen: false) + .onEnter = false, + child: IntrinsicHeight( + child: Stack( + alignment: AlignmentDirectional.center, + fit: StackFit.expand, + children: children, + )), + ); + }, + ); + } +} + +class _CardContainerNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardContainerNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart index e41f9bc9ac..9b3f281130 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_accessory.dart @@ -8,6 +8,8 @@ import 'package:styled_widget/styled_widget.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'cell_builder.dart'; + class GridCellAccessoryBuildContext { final BuildContext anchorContext; final bool isCellEditing; @@ -57,18 +59,6 @@ class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory { bool enable() => !isCellEditing; } -typedef AccessoryBuilder = List Function( - GridCellAccessoryBuildContext buildContext); - -abstract class CellAccessory extends Widget { - const CellAccessory({Key? key}) : super(key: key); - - // The hover will show if the isHover's value is true - ValueNotifier? get onAccessoryHover; - - AccessoryBuilder? get accessoryBuilder; -} - class AccessoryHover extends StatefulWidget { final CellAccessory child; final EdgeInsets contentPadding; diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart index 6c3fa38bc1..0a7c3a48a7 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_builder.dart @@ -94,6 +94,18 @@ abstract class CellEditable { ValueNotifier get onCellEditing; } +typedef AccessoryBuilder = List Function( + GridCellAccessoryBuildContext buildContext); + +abstract class CellAccessory extends Widget { + const CellAccessory({Key? key}) : super(key: key); + + // The hover will show if the isHover's value is true + ValueNotifier? get onAccessoryHover; + + AccessoryBuilder? get accessoryBuilder; +} + abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellEditable, CellShortcuts { GridCellWidget({Key? key}) : super(key: key) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart index b2d174e3e2..ed09ec3f36 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/cell_container.dart @@ -25,24 +25,28 @@ class CellContainer extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProxyProvider( - create: (_) => CellContainerNotifier(child), + _CellContainerNotifier>( + create: (_) => _CellContainerNotifier(child), update: (_, rowStateNotifier, cellStateNotifier) => cellStateNotifier!..onEnter = rowStateNotifier.onEnter, - child: Selector( + child: Selector<_CellContainerNotifier, bool>( selector: (context, notifier) => notifier.isFocus, builder: (context, isFocus, _) { Widget container = Center(child: GridCellShortcuts(child: child)); if (accessoryBuilder != null) { - final accessories = accessoryBuilder!(GridCellAccessoryBuildContext( - anchorContext: context, - isCellEditing: isFocus, - )); + final accessories = accessoryBuilder!( + GridCellAccessoryBuildContext( + anchorContext: context, + isCellEditing: isFocus, + ), + ); if (accessories.isNotEmpty) { - container = - CellEnterRegion(child: container, accessories: accessories); + container = _GridCellEnterRegion( + child: container, + accessories: accessories, + ); } } @@ -74,16 +78,16 @@ class CellContainer extends StatelessWidget { } } -class CellEnterRegion extends StatelessWidget { +class _GridCellEnterRegion extends StatelessWidget { final Widget child; final List accessories; - const CellEnterRegion( + const _GridCellEnterRegion( {required this.child, required this.accessories, Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Selector( + return Selector<_CellContainerNotifier, bool>( selector: (context, notifier) => notifier.onEnter, builder: (context, onEnter, _) { List children = [child]; @@ -95,10 +99,10 @@ class CellEnterRegion extends StatelessWidget { return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (p) => - Provider.of(context, listen: false) + Provider.of<_CellContainerNotifier>(context, listen: false) .onEnter = true, onExit: (p) => - Provider.of(context, listen: false) + Provider.of<_CellContainerNotifier>(context, listen: false) .onEnter = false, child: Stack( alignment: AlignmentDirectional.center, @@ -111,13 +115,13 @@ class CellEnterRegion extends StatelessWidget { } } -class CellContainerNotifier extends ChangeNotifier { +class _CellContainerNotifier extends ChangeNotifier { final CellEditable cellEditable; VoidCallback? _onCellFocusListener; bool _isFocus = false; bool _onEnter = false; - CellContainerNotifier(this.cellEditable) { + _CellContainerNotifier(this.cellEditable) { _onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value; cellEditable.onCellFocus.addListener(_onCellFocusListener!); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 3864c1a6a0..c96b5e1526 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -181,28 +181,27 @@ class RowContent extends StatelessWidget { return gridCellMap.values.map( (cellId) { final GridCellWidget child = builder.build(cellId); - accessoryBuilder(GridCellAccessoryBuildContext buildContext) { - final builder = child.accessoryBuilder; - List accessories = []; - if (cellId.field.isPrimary) { - accessories.add(PrimaryCellAccessory( - onTapCallback: onExpand, - isCellEditing: buildContext.isCellEditing, - )); - } - - if (builder != null) { - accessories.addAll(builder(buildContext)); - } - return accessories; - } return CellContainer( width: cellId.field.width.toDouble(), child: child, rowStateNotifier: Provider.of(context, listen: false), - accessoryBuilder: accessoryBuilder, + accessoryBuilder: (buildContext) { + final builder = child.accessoryBuilder; + List accessories = []; + if (cellId.field.isPrimary) { + accessories.add(PrimaryCellAccessory( + onTapCallback: onExpand, + isCellEditing: buildContext.isCellEditing, + )); + } + + if (builder != null) { + accessories.addAll(builder(buildContext)); + } + return accessories; + }, ); }, ).toList(); diff --git a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart index 282e6028c5..83f75d2a0e 100644 --- a/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart +++ b/frontend/app_flowy/packages/appflowy_board/example/lib/multi_board_list_example.dart @@ -23,18 +23,19 @@ class _MultiBoardListExampleState extends State { @override void initState() { - final column1 = AFBoardColumnData(id: "To Do", items: [ + List a = [ TextItem("Card 1"), TextItem("Card 2"), - RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), + // RichTextItem(title: "Card 3", subtitle: 'Aug 1, 2020 4:05 PM'), TextItem("Card 4"), - ]); - final column2 = AFBoardColumnData(id: "In Progress", items: [ - RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), - TextItem("Card 6"), + ]; + final column1 = AFBoardColumnData(id: "To Do", items: a); + final column2 = AFBoardColumnData(id: "In Progress", items: [ + // RichTextItem(title: "Card 5", subtitle: 'Aug 1, 2020 4:05 PM'), + // TextItem("Card 6"), ]); - final column3 = AFBoardColumnData(id: "Done", items: []); + final column3 = AFBoardColumnData(id: "Done", items: []); boardDataController.addColumn(column1); boardDataController.addColumn(column2); From f6263f758f9eaabfea43833e8035c9c25e90cf7a Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 11:57:14 +0800 Subject: [PATCH 110/224] fix: multi scrollview warning --- .../app_flowy/lib/plugins/board/application/board_bloc.dart | 5 +++++ .../lib/plugins/board/application/board_data_controller.dart | 4 ++-- frontend/app_flowy/lib/plugins/board/board.dart | 2 +- .../app_flowy/lib/plugins/board/presentation/board_page.dart | 3 ++- .../app_flowy/lib/plugins/board/presentation/card/card.dart | 3 ++- .../packages/appflowy_board/lib/src/widgets/board.dart | 4 ++++ 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 08b519c1a3..09f85df732 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -207,3 +207,8 @@ class BoardColumnItem extends AFColumnItem { @override String get id => row.id; } + +class CreateCardItem extends AFColumnItem { + @override + String get id => '$CreateCardItem'; +} diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 4a0333f30b..da4cc54132 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -89,8 +89,8 @@ class BoardDataController { ); } - void createRow() { - _gridFFIService.createRow(); + Future> createRow() { + return _gridFFIService.createRow(); } Future dispose() async { diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 36d181ae3e..2954a7cbf9 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index b373f65604..6dcfe8267c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -54,10 +54,11 @@ class BoardContent extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( key: UniqueKey(), + scrollController: ScrollController(), dataController: context.read().boardDataController, headerBuilder: _buildHeader, footBuilder: _buildFooter, - cardBuilder: _buildCard, + cardBuilder: (_, data) => _buildCard(context, data), columnConstraints: const BoxConstraints.tightFor(width: 240), config: AFBoardConfig( columnBackgroundColor: HexColor.fromHex('#F7F8FC'), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 2c17d5e3c8..dfb9ab42d7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -3,6 +3,7 @@ import 'package:app_flowy/plugins/board/application/card/card_data_controller.da import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flowy_sdk/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'card_cell_builder.dart'; @@ -79,6 +80,6 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { @override void onTap(BuildContext context) { - print('show options'); + Log.debug('show options'); } } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index 09f1590f3d..f4ce09fc2c 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -46,6 +46,8 @@ class AFBoard extends StatelessWidget { /// final BoardPhantomController phantomController; + final ScrollController? scrollController; + final AFBoardConfig config; AFBoard({ @@ -54,6 +56,7 @@ class AFBoard extends StatelessWidget { this.background, this.footBuilder, this.headerBuilder, + this.scrollController, this.columnConstraints = const BoxConstraints(maxWidth: 200), this.config = const AFBoardConfig(), Key? key, @@ -69,6 +72,7 @@ class AFBoard extends StatelessWidget { return BoardContent( config: config, dataController: dataController, + scrollController: scrollController, background: background, delegate: phantomController, columnConstraints: columnConstraints, From c001a7447672dc1edb070930f66e2270ddbd5b4a Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 14:32:30 +0800 Subject: [PATCH 111/224] fix: rust unit test compile errors --- .../flowy-grid/tests/grid/block_test/util.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index 9b97afbf56..a733926228 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -26,18 +26,14 @@ impl<'a> GridRowTestBuilder<'a> { pub fn insert_text_cell(&mut self, data: &str) -> String { let text_field = self.field_rev_with_type(&FieldType::RichText); - self.inner_builder - .insert_cell(&text_field.id, data.to_string()) - .unwrap(); + self.inner_builder.insert_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_rev_with_type(&FieldType::Number); - self.inner_builder - .insert_cell(&number_field.id, data.to_string()) - .unwrap(); + self.inner_builder.insert_cell(&number_field.id, data.to_string()); number_field.id.clone() } @@ -48,22 +44,20 @@ impl<'a> GridRowTestBuilder<'a> { }) .unwrap(); let date_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.insert_cell(&date_field.id, value).unwrap(); + self.inner_builder.insert_cell(&date_field.id, value); date_field.id.clone() } pub fn insert_checkbox_cell(&mut self, data: &str) -> String { let checkbox_field = self.field_rev_with_type(&FieldType::Checkbox); - self.inner_builder - .insert_cell(&checkbox_field.id, data.to_string()) - .unwrap(); + self.inner_builder.insert_cell(&checkbox_field.id, data.to_string()); checkbox_field.id.clone() } pub fn insert_url_cell(&mut self, data: &str) -> String { let url_field = self.field_rev_with_type(&FieldType::URL); - self.inner_builder.insert_cell(&url_field.id, data.to_string()).unwrap(); + self.inner_builder.insert_cell(&url_field.id, data.to_string()); url_field.id.clone() } From 57ede798d83b4f7488c2754e9cae6a3f3a643e4c Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 14:59:50 +0800 Subject: [PATCH 112/224] refactor: remove UpdateRowPB, refactor RowInfo class --- .../plugins/board/application/board_bloc.dart | 12 ++- .../application/board_data_controller.dart | 45 ++++----- .../board/application/card/card_bloc.dart | 4 +- .../card/card_data_controller.dart | 2 +- .../app_flowy/lib/plugins/board/board.dart | 2 +- .../grid/application/block/block_cache.dart | 4 +- .../plugins/grid/application/grid_bloc.dart | 4 +- .../application/grid_data_controller.dart | 4 +- .../row/row_action_sheet_bloc.dart | 10 +- .../grid/application/row/row_bloc.dart | 6 +- .../grid/application/row/row_cache.dart | 92 ++++++++++--------- .../application/row/row_data_controller.dart | 6 +- .../plugins/grid/presentation/grid_page.dart | 4 +- .../presentation/widgets/row/grid_row.dart | 2 +- .../widgets/row/row_action_sheet.dart | 2 +- .../flowy-grid/src/entities/block_entities.rs | 45 ++------- .../rust-lib/flowy-grid/src/event_handler.rs | 2 +- .../flowy-grid/src/services/block_manager.rs | 27 +++--- .../flowy-grid/src/services/row/row_loader.rs | 4 +- 19 files changed, 118 insertions(+), 159 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 09f85df732..baefecf723 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -52,8 +52,12 @@ class BoardBloc extends Bloc { _startListening(); await _loadGrid(emit); }, - createRow: () { - _dataController.createRow(); + createRow: () async { + final result = await _dataController.createRow(); + result.fold( + (rowPB) => null, + (err) => Log.error(err), + ); }, didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); @@ -99,7 +103,7 @@ class BoardBloc extends Bloc { boardDataController.addColumns(columns); }, - onRowsChanged: (List rowInfos, RowChangeReason reason) { + onRowsChanged: (List rowInfos, RowsChangedReason reason) { add(BoardEvent.didReceiveRows(rowInfos)); }, onError: (err) { @@ -156,6 +160,7 @@ class BoardState with _$BoardState { required String gridId, required Option grid, required List groups, + required Option editingRow, required List rowInfos, required GridLoadingState loadingState, }) = _BoardState; @@ -165,6 +170,7 @@ class BoardState with _$BoardState { groups: [], grid: none(), gridId: gridId, + editingRow: none(), loadingState: const _Loading(), ); } diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index da4cc54132..fe6e1b0bdf 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/application/block/block_cache.dart'; import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; -import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'dart:async'; @@ -15,8 +14,8 @@ typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef OnGroupChanged = void Function(List); typedef OnRowsChanged = void Function( - List rowInfos, - RowChangeReason, + List, + RowsChangedReason, ); typedef OnError = void Function(FlowyError); @@ -73,11 +72,11 @@ class BoardDataController { () => result.fold( (grid) async { _onGridChanged?.call(grid); - _initialBlocks(grid.blocks); + return await _loadFields(grid).then((result) { return result.fold( (l) { - _loadGroups(); + _loadGroups(grid.blocks); return left(l); }, (err) => right(err), @@ -102,29 +101,6 @@ class BoardDataController { } } - void _initialBlocks(List blocks) { - for (final block in blocks) { - if (_blocks[block.id] != null) { - Log.warn("Initial duplicate block's cache: ${block.id}"); - return; - } - - final cache = GridBlockCache( - gridId: gridId, - block: block, - fieldCache: fieldCache, - ); - - cache.addListener( - onChangeReason: (reason) { - _onRowsChanged?.call(rowInfos, reason); - }, - ); - - _blocks[block.id] = cache; - } - } - Future> _loadFields(GridPB grid) async { final result = await _gridFFIService.getFields(fieldIds: grid.fields); return Future( @@ -139,7 +115,18 @@ class BoardDataController { ); } - Future _loadGroups() async { + Future _loadGroups(List blocks) async { + for (final block in blocks) { + final cache = GridBlockCache( + gridId: gridId, + block: block, + fieldCache: fieldCache, + ); + + // cache.addListener(onRowsChanged: (rows, reason) {}) + _blocks[block.id] = cache; + } + final result = await _gridFFIService.loadGroups(); return Future( () => result.fold( diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart index 9ba66c2aab..1f66ebe335 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -74,7 +74,7 @@ class BoardCardEvent with _$BoardCardEvent { const factory BoardCardEvent.initial() = _InitialRow; const factory BoardCardEvent.createRow() = _CreateRow; const factory BoardCardEvent.didReceiveCells( - GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells; + GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells; } @freezed @@ -83,7 +83,7 @@ class BoardCardState with _$BoardCardState { required RowPB rowPB, required GridCellMap gridCellMap, required UnmodifiableListView cells, - RowChangeReason? changeReason, + RowsChangedReason? changeReason, }) = _BoardCardState; factory BoardCardState.initial(RowPB rowPB, GridCellMap cellDataMap) => diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart index d9ac41f10b..f362fdf0e6 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_data_controller.dart @@ -6,7 +6,7 @@ import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flutter/foundation.dart'; -typedef OnCardChanged = void Function(GridCellMap, RowChangeReason); +typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason); class CardDataController extends BoardCellBuilderDelegate { final RowPB rowPB; diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 2954a7cbf9..36d181ae3e 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart index ecfea7e119..b639700b5f 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/block/block_cache.dart @@ -42,7 +42,7 @@ class GridBlockCache { } void addListener({ - required void Function(RowChangeReason) onChangeReason, + required void Function(RowsChangedReason) onRowsChanged, bool Function()? listenWhen, }) { _rowCache.onRowsChanged((reason) { @@ -50,7 +50,7 @@ class GridBlockCache { return; } - onChangeReason(reason); + onRowsChanged(reason); }); } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart index 73d2079a6b..74f23a1b3e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart @@ -98,7 +98,7 @@ class GridEvent with _$GridEvent { const factory GridEvent.createRow() = _CreateRow; const factory GridEvent.didReceiveRowUpdate( List rows, - RowChangeReason listState, + RowsChangedReason listState, ) = _DidReceiveRowUpdate; const factory GridEvent.didReceiveFieldUpdate( UnmodifiableListView fields, @@ -117,7 +117,7 @@ class GridState with _$GridState { required GridFieldEquatable fields, required List rowInfos, required GridLoadingState loadingState, - required RowChangeReason reason, + required RowsChangedReason reason, }) = _GridState; factory GridState.initial(String gridId) => GridState( diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index aae6dc684e..9de6c65997 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -18,7 +18,7 @@ typedef OnGridChanged = void Function(GridPB); typedef OnRowsChanged = void Function( List rowInfos, - RowChangeReason, + RowsChangedReason, ); typedef ListenOnRowChangedCondition = bool Function(); @@ -105,7 +105,7 @@ class GridDataController { ); cache.addListener( - onChangeReason: (reason) { + onRowsChanged: (reason) { _onRowChanged?.call(rowInfos, reason); }, ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart index 7e3e9a21bd..e586f65bd4 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart @@ -14,13 +14,13 @@ class RowActionSheetBloc extends Bloc { final RowFFIService _rowService; - RowActionSheetBloc({required RowInfo rowData}) + RowActionSheetBloc({required RowInfo rowInfo}) : _rowService = RowFFIService( - gridId: rowData.gridId, - blockId: rowData.blockId, - rowId: rowData.id, + gridId: rowInfo.gridId, + blockId: rowInfo.blockId, + rowId: rowInfo.rowPB.id, ), - super(RowActionSheetState.initial(rowData)) { + super(RowActionSheetState.initial(rowInfo)) { on( (event, emit) async { await event.map( diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index 372287fd1f..6716967e5e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -21,7 +21,7 @@ class RowBloc extends Bloc { }) : _rowService = RowFFIService( gridId: rowInfo.gridId, blockId: rowInfo.blockId, - rowId: rowInfo.id, + rowId: rowInfo.rowPB.id, ), _dataController = dataController, super(RowState.initial(rowInfo, dataController.loadData())) { @@ -71,7 +71,7 @@ class RowEvent with _$RowEvent { const factory RowEvent.initial() = _InitialRow; const factory RowEvent.createRow() = _CreateRow; const factory RowEvent.didReceiveCells( - GridCellMap gridCellMap, RowChangeReason reason) = _DidReceiveCells; + GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells; } @freezed @@ -80,7 +80,7 @@ class RowState with _$RowState { required RowInfo rowInfo, required GridCellMap gridCellMap, required UnmodifiableListView cells, - RowChangeReason? changeReason, + RowsChangedReason? changeReason, }) = _RowState; factory RowState.initial(RowInfo rowInfo, GridCellMap cellDataMap) => diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index ec212148d1..74e11e409e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -51,11 +51,9 @@ class GridRowCache { _fieldNotifier = notifier { // notifier.onRowFieldsChanged(() => _rowChangeReasonNotifier - .receive(const RowChangeReason.fieldDidChange())); + .receive(const RowsChangedReason.fieldDidChange())); notifier.onRowFieldChanged((field) => _cellCache.remove(field.id)); - _rowInfos = block.rows - .map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble())) - .toList(); + _rowInfos = block.rows.map((rowPB) => buildGridRow(rowPB)).toList(); } Future dispose() async { @@ -85,16 +83,16 @@ class GridRowCache { for (var rowId in deletedRows) rowId: rowId }; - _rowInfos.asMap().forEach((index, row) { - if (deletedRowByRowId[row.id] == null) { - newRows.add(row); + _rowInfos.asMap().forEach((index, RowInfo rowInfo) { + if (deletedRowByRowId[rowInfo.rowPB.id] == null) { + newRows.add(rowInfo); } else { - _rowByRowId.remove(row.id); - deletedIndex.add(DeletedIndex(index: index, row: row)); + _rowByRowId.remove(rowInfo.rowPB.id); + deletedIndex.add(DeletedIndex(index: index, row: rowInfo)); } }); _rowInfos = newRows; - _rowChangeReasonNotifier.receive(RowChangeReason.delete(deletedIndex)); + _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex)); } void _insertRows(List insertRows) { @@ -103,39 +101,42 @@ class GridRowCache { } InsertedIndexs insertIndexs = []; - for (final insertRow in insertRows) { + for (final InsertedRowPB insertRow in insertRows) { final insertIndex = InsertedIndex( index: insertRow.index, - rowId: insertRow.rowId, + rowId: insertRow.row.id, ); insertIndexs.add(insertIndex); - _rowInfos.insert(insertRow.index, - (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); + _rowInfos.insert( + insertRow.index, + (buildGridRow(insertRow.row)), + ); } - _rowChangeReasonNotifier.receive(RowChangeReason.insert(insertIndexs)); + _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs)); } - void _updateRows(List updatedRows) { + void _updateRows(List updatedRows) { if (updatedRows.isEmpty) { return; } final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - for (final updatedRow in updatedRows) { - final rowId = updatedRow.rowId; - final index = _rowInfos.indexWhere((row) => row.id == rowId); + for (final RowPB updatedRow in updatedRows) { + final rowId = updatedRow.id; + final index = _rowInfos.indexWhere( + (rowInfo) => rowInfo.rowPB.id == rowId, + ); if (index != -1) { - _rowByRowId[rowId] = updatedRow.row; + _rowByRowId[rowId] = updatedRow; _rowInfos.removeAt(index); - _rowInfos.insert( - index, buildGridRow(rowId, updatedRow.row.height.toDouble())); + _rowInfos.insert(index, buildGridRow(updatedRow)); updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); } } - _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs)); + _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); } void _hideRows(List hideRows) {} @@ -143,7 +144,7 @@ class GridRowCache { void _showRows(List visibleRows) {} void onRowsChanged( - void Function(RowChangeReason) onRowChanged, + void Function(RowsChangedReason) onRowChanged, ) { _rowChangeReasonNotifier.addListener(() { onRowChanged(_rowChangeReasonNotifier.reason); @@ -152,7 +153,7 @@ class GridRowCache { RowUpdateCallback addListener({ required String rowId, - void Function(GridCellMap, RowChangeReason)? onCellUpdated, + void Function(GridCellMap, RowsChangedReason)? onCellUpdated, bool Function()? listenWhen, }) { listenerHandler() async { @@ -230,40 +231,43 @@ class GridRowCache { _rowByRowId[updatedRow.id] = updatedRow; final index = - _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id); + _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id); if (index != -1) { // update the corresponding row in _rows if they are not the same - if (_rowInfos[index].rawRow != updatedRow) { - final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow); - _rowInfos.insert(index, row); + if (_rowInfos[index].rowPB != updatedRow) { + final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow); + _rowInfos.insert(index, rowInfo); // Calculate the update index final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id); + updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex( + index: index, + rowId: rowInfo.rowPB.id, + ); // - _rowChangeReasonNotifier.receive(RowChangeReason.update(updatedIndexs)); + _rowChangeReasonNotifier + .receive(RowsChangedReason.update(updatedIndexs)); } } } - RowInfo buildGridRow(String rowId, double rowHeight) { + RowInfo buildGridRow(RowPB rowPB) { return RowInfo( gridId: gridId, blockId: block.id, fields: _fieldNotifier.fields, - id: rowId, - height: rowHeight, + rowPB: rowPB, ); } } class _RowChangesetNotifier extends ChangeNotifier { - RowChangeReason reason = const InitialListState(); + RowsChangedReason reason = const InitialListState(); _RowChangesetNotifier(); - void receive(RowChangeReason newReason) { + void receive(RowsChangedReason newReason) { reason = newReason; reason.map( insert: (_) => notifyListeners(), @@ -280,10 +284,8 @@ class RowInfo with _$RowInfo { const factory RowInfo({ required String gridId, required String blockId, - required String id, required UnmodifiableListView fields, - required double height, - RowPB? rawRow, + required RowPB rowPB, }) = _RowInfo; } @@ -292,12 +294,12 @@ typedef DeletedIndexs = List; typedef UpdatedIndexs = LinkedHashMap; @freezed -class RowChangeReason with _$RowChangeReason { - const factory RowChangeReason.insert(InsertedIndexs items) = _Insert; - const factory RowChangeReason.delete(DeletedIndexs items) = _Delete; - const factory RowChangeReason.update(UpdatedIndexs indexs) = _Update; - const factory RowChangeReason.fieldDidChange() = _FieldDidChange; - const factory RowChangeReason.initial() = InitialListState; +class RowsChangedReason with _$RowsChangedReason { + const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert; + const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete; + const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update; + const factory RowsChangedReason.fieldDidChange() = _FieldDidChange; + const factory RowsChangedReason.initial() = InitialListState; } class InsertedIndex { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart index 78783fc894..b4618b397a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_data_controller.dart @@ -5,7 +5,7 @@ import '../cell/cell_service/cell_service.dart'; import '../field/field_cache.dart'; import 'row_cache.dart'; -typedef OnRowChanged = void Function(GridCellMap, RowChangeReason); +typedef OnRowChanged = void Function(GridCellMap, RowsChangedReason); class GridRowDataController extends GridCellBuilderDelegate { final RowInfo rowInfo; @@ -21,12 +21,12 @@ class GridRowDataController extends GridCellBuilderDelegate { _rowCache = rowCache; GridCellMap loadData() { - return _rowCache.loadGridCells(rowInfo.id); + return _rowCache.loadGridCells(rowInfo.rowPB.id); } void addListener({OnRowChanged? onRowChanged}) { _onRowChangedListeners.add(_rowCache.addListener( - rowId: rowInfo.id, + rowId: rowInfo.rowPB.id, onCellUpdated: onRowChanged, )); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index 8709b395b2..fe9b245567 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -240,7 +240,7 @@ class _GridRowsState extends State<_GridRows> { Animation animation, ) { final rowCache = - context.read().getRowCache(rowInfo.blockId, rowInfo.id); + context.read().getRowCache(rowInfo.blockId, rowInfo.rowPB.id); /// Return placeholder widget if the rowCache is null. if (rowCache == null) return const SizedBox(); @@ -267,7 +267,7 @@ class _GridRowsState extends State<_GridRows> { cellBuilder, ); }, - key: ValueKey(rowInfo.id), + key: ValueKey(rowInfo.rowPB.id), ), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index c96b5e1526..a4bf813fe5 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -52,7 +52,7 @@ class _GridRowWidgetState extends State { value: _rowBloc, child: _RowEnterRegion( child: BlocBuilder( - buildWhen: (p, c) => p.rowInfo.height != c.rowInfo.height, + buildWhen: (p, c) => p.rowInfo.rowPB.height != c.rowInfo.rowPB.height, builder: (context, state) { final children = [ const _RowLeading(), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart index 720aac0dc0..8296add94e 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart @@ -21,7 +21,7 @@ class GridRowActionSheet extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => RowActionSheetBloc(rowData: rowData), + create: (context) => RowActionSheetBloc(rowInfo: rowData), child: BlocBuilder( builder: (context, state) { final cells = _RowAction.values diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index ad89532b02..b712a701df 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -106,48 +106,15 @@ impl std::convert::From> for RepeatedBlockPB { #[derive(Debug, Clone, Default, ProtoBuf)] pub struct InsertedRowPB { #[pb(index = 1)] - pub block_id: String, + pub row: RowPB, - #[pb(index = 2)] - pub row_id: String, - - #[pb(index = 3)] - pub height: i32, - - #[pb(index = 4, one_of)] + #[pb(index = 2, one_of)] pub index: Option, } -#[derive(Debug, Default, ProtoBuf)] -pub struct UpdatedRowPB { - #[pb(index = 1)] - pub block_id: String, - - #[pb(index = 2)] - pub row_id: String, - - #[pb(index = 3)] - pub row: RowPB, -} - -impl UpdatedRowPB { - pub fn new(row_rev: &RowRevision, row: RowPB) -> Self { - Self { - row_id: row_rev.id.clone(), - block_id: row_rev.block_id.clone(), - row, - } - } -} - impl std::convert::From for InsertedRowPB { - fn from(row_info: RowPB) -> Self { - Self { - row_id: row_info.id, - block_id: row_info.block_id, - height: row_info.height, - index: None, - } + fn from(row: RowPB) -> Self { + Self { row, index: None } } } @@ -170,7 +137,7 @@ pub struct GridBlockChangesetPB { pub deleted_rows: Vec, #[pb(index = 4)] - pub updated_rows: Vec, + pub updated_rows: Vec, #[pb(index = 5)] pub visible_rows: Vec, @@ -195,7 +162,7 @@ impl GridBlockChangesetPB { } } - pub fn update(block_id: &str, updated_rows: Vec) -> Self { + pub fn update(block_id: &str, updated_rows: Vec) -> Self { Self { block_id: block_id.to_owned(), updated_rows, diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 86b8e8892c..3c24564c6d 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -235,7 +235,7 @@ pub(crate) async fn get_row_handler( let row = editor .get_row_rev(¶ms.row_id) .await? - .and_then(make_row_from_row_rev); + .and_then(|row_rev| Some(make_row_from_row_rev(row_rev))); data_result(OptionalRowPB { row }) } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index d7c01a6a09..afeb58c5d4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -1,9 +1,9 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB, UpdatedRowPB}; +use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB}; use crate::manager::GridUser; use crate::services::block_revision_editor::{GridBlockRevisionCompactor, GridBlockRevisionEditor}; use crate::services::persistence::block_index::BlockIndexCache; -use crate::services::row::{block_from_row_orders, GridBlockSnapshot}; +use crate::services::row::{block_from_row_orders, make_row_from_row_rev, GridBlockSnapshot}; use dashmap::DashMap; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ @@ -110,20 +110,18 @@ impl GridBlockManager { pub async fn update_row(&self, changeset: RowMetaChangeset, row_builder: F) -> FlowyResult<()> where - F: FnOnce(Arc) -> Option, + F: FnOnce(Arc) -> RowPB, { let editor = self.get_editor_from_row_id(&changeset.row_id).await?; let _ = editor.update_row(changeset.clone()).await?; match editor.get_row_rev(&changeset.row_id).await? { None => tracing::error!("Internal error: can't find the row with id: {}", changeset.row_id), Some(row_rev) => { - if let Some(row) = row_builder(row_rev.clone()) { - let row_order = UpdatedRowPB::new(&row_rev, row); - let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_order]); - let _ = self - .notify_did_update_block(&editor.block_id, block_order_changeset) - .await?; - } + let block_order_changeset = + GridBlockChangesetPB::update(&editor.block_id, vec![row_builder(row_rev.clone())]); + let _ = self + .notify_did_update_block(&editor.block_id, block_order_changeset) + .await?; } } Ok(()) @@ -170,17 +168,16 @@ impl GridBlockManager { match editor.get_row_revs(Some(vec![Cow::Borrowed(row_id)])).await?.pop() { None => {} Some(row_rev) => { + let delete_row_id = row_rev.id.clone(); let insert_row = InsertedRowPB { - block_id: row_rev.block_id.clone(), - row_id: row_rev.id.clone(), index: Some(to as i32), - height: row_rev.height, + row: make_row_from_row_rev(row_rev), }; let notified_changeset = GridBlockChangesetPB { block_id: editor.block_id.clone(), inserted_rows: vec![insert_row], - deleted_rows: vec![row_rev.id.clone()], + deleted_rows: vec![delete_row_id], ..Default::default() }; @@ -195,7 +192,7 @@ impl GridBlockManager { pub async fn update_cell(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()> where - F: FnOnce(Arc) -> Option, + F: FnOnce(Arc) -> RowPB, { let row_changeset: RowMetaChangeset = changeset.clone().into(); let _ = self.update_row(row_changeset, row_builder).await?; diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs index 24c2cda68a..1a0d0eaff6 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs @@ -39,8 +39,8 @@ pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc]) -> Ve row_revs.iter().map(RowPB::from).collect::>() } -pub(crate) fn make_row_from_row_rev(row_rev: Arc) -> Option { - make_rows_from_row_revs(&[row_rev]).pop() +pub(crate) fn make_row_from_row_rev(row_rev: Arc) -> RowPB { + make_rows_from_row_revs(&[row_rev]).pop().unwrap() } pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc]) -> Vec { From dc53cb00dd1c209867db62623ab2505c7821d4dc Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 16:23:44 +0800 Subject: [PATCH 113/224] chore: create the default group for the rows that are not belong to any groups --- .../plugins/board/application/board_bloc.dart | 12 ++++++- .../application/board_data_controller.dart | 4 ++- .../board/presentation/board_page.dart | 24 ++++++++++---- .../plugins/board/presentation/card/card.dart | 7 +++- .../presentation/card/card_container.dart | 6 +++- .../rust-lib/flowy-grid/src/event_handler.rs | 6 ++-- .../group/group_generator/generator.rs | 33 ++++++++++++++++--- 7 files changed, 73 insertions(+), 19 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index baefecf723..1db180d4e4 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -55,10 +55,19 @@ class BoardBloc extends Bloc { createRow: () async { final result = await _dataController.createRow(); result.fold( - (rowPB) => null, + (rowPB) { + emit(state.copyWith(editingRow: some(rowPB))); + }, (err) => Log.error(err), ); }, + endEditRow: (rowId) { + assert(state.editingRow.isSome()); + state.editingRow.fold(() => null, (row) { + assert(row.id == rowId); + emit(state.copyWith(editingRow: none())); + }); + }, didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, @@ -145,6 +154,7 @@ class BoardBloc extends Bloc { class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = InitialGrid; const factory BoardEvent.createRow() = _CreateRow; + const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.didReceiveGroups(List groups) = _DidReceiveGroup; const factory BoardEvent.didReceiveRows(List rowInfos) = diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index fe6e1b0bdf..619d16f99e 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -123,7 +123,9 @@ class BoardDataController { fieldCache: fieldCache, ); - // cache.addListener(onRowsChanged: (rows, reason) {}) + cache.addListener(onRowsChanged: (reason) { + _onRowsChanged?.call(rowInfos, reason); + }); _blocks[block.id] = cache; } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 6dcfe8267c..bf04c22f57 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -53,7 +53,7 @@ class BoardContent extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), child: AFBoard( - key: UniqueKey(), + // key: UniqueKey(), scrollController: ScrollController(), dataController: context.read().boardDataController, headerBuilder: _buildHeader, @@ -83,11 +83,13 @@ class BoardContent extends StatelessWidget { Widget _buildFooter(BuildContext context, AFBoardColumnData columnData) { return AppFlowyColumnFooter( - icon: const Icon(Icons.add, size: 20), - title: const Text('New'), - height: 50, - margin: config.columnItemPadding, - ); + icon: const Icon(Icons.add, size: 20), + title: const Text('New'), + height: 50, + margin: config.columnItemPadding, + onAddButtonClick: () { + context.read().add(const BoardEvent.createRow()); + }); } Widget _buildCard(BuildContext context, AFColumnItem item) { @@ -106,13 +108,21 @@ class BoardContent extends StatelessWidget { ); final cellBuilder = BoardCellBuilder(cardController); + final isEditing = context.read().state.editingRow.fold( + () => false, + (editingRow) => editingRow.id == rowPB.id, + ); return AppFlowyColumnItemCard( key: ObjectKey(item), child: BoardCard( + gridId: gridId, + isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, - gridId: gridId, + onEditEditing: (rowId) { + context.read().add(BoardEvent.endEditRow(rowId)); + }, ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index dfb9ab42d7..2a4006e68c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -9,15 +9,21 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'card_cell_builder.dart'; import 'card_container.dart'; +typedef OnEndEditing = void Function(String rowId); + class BoardCard extends StatefulWidget { final String gridId; + final bool isEditing; final CardDataController dataController; final BoardCellBuilder cellBuilder; + final OnEndEditing onEditEditing; const BoardCard({ required this.gridId, + required this.isEditing, required this.dataController, required this.cellBuilder, + required this.onEditEditing, Key? key, }) : super(key: key); @@ -60,7 +66,6 @@ class _BoardCardState extends State { return cellMap.values.map( (cellId) { final child = widget.cellBuilder.buildCell(cellId); - return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: child, diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index 13f3af2195..ce8df101a5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -29,9 +29,13 @@ class BoardCardContainer extends StatelessWidget { ); } } + return Padding( padding: const EdgeInsets.all(8), - child: container, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: container, + ), ); }, ), diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 3c24564c6d..e834c4652e 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -266,11 +266,11 @@ pub(crate) async fn duplicate_row_handler( pub(crate) async fn create_row_handler( data: Data, manager: AppData>, -) -> Result<(), FlowyError> { +) -> DataResult { let params: CreateRowParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(params.grid_id.as_ref())?; - let _ = editor.create_row(params.start_row_id).await?; - Ok(()) + let row = editor.create_row(params.start_row_id).await?; + data_result(row) } // #[tracing::instrument(level = "debug", skip_all, err)] diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index 4740e6e7bb..1682bd9fbb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -34,9 +34,12 @@ pub trait GroupGenerator { ) -> Vec; } +const DEFAULT_GROUP_ID: &str = "default_group"; + pub struct GroupController { pub field_rev: Arc, pub groups: IndexMap, + pub default_group: Group, pub type_option: Option, pub configuration: Option, group_action_phantom: PhantomData, @@ -78,9 +81,18 @@ where let field_type_rev = field_rev.field_type_rev; let type_option = field_rev.get_type_option_entry::(field_type_rev); let groups = G::gen_groups(&configuration, &type_option, cell_content_provider); + + let default_group = Group { + id: DEFAULT_GROUP_ID.to_owned(), + desc: format!("No {}", field_rev.name), + rows: vec![], + content: "".to_string(), + }; + Ok(Self { field_rev, groups: groups.into_iter().map(|group| (group.id.clone(), group)).collect(), + default_group, type_option, configuration, group_action_phantom: PhantomData, @@ -89,7 +101,12 @@ where } pub fn take_groups(self) -> Vec { - self.groups.into_values().collect() + let default_group = self.default_group; + let mut groups: Vec = self.groups.into_values().collect(); + if !default_group.rows.is_empty() { + groups.push(default_group); + } + groups } } @@ -102,6 +119,7 @@ where if self.configuration.is_none() { return Ok(()); } + tracing::debug!("group {} rows", rows.len()); for row in rows { if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { @@ -115,15 +133,20 @@ where row: row.into(), group_id: group.id.clone(), }); - break; } } - for record in records { - if let Some(group) = self.groups.get_mut(&record.group_id) { - group.rows.push(record.row); + if records.is_empty() { + self.default_group.rows.push(row.into()); + } else { + for record in records { + if let Some(group) = self.groups.get_mut(&record.group_id) { + group.rows.push(record.row); + } } } + } else { + self.default_group.rows.push(row.into()); } } From f0914cd6f1c343d14051cdc2e436ebbdc04e1612 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sat, 13 Aug 2022 23:26:42 +0800 Subject: [PATCH 114/224] chore: add group action handler to intercept the create row progress --- .../flowy-grid/src/entities/block_entities.rs | 4 +- .../src/entities/group_entities/board_card.rs | 29 +++++ .../src/entities/group_entities/mod.rs | 2 + .../rust-lib/flowy-grid/src/event_handler.rs | 11 ++ frontend/rust-lib/flowy-grid/src/event_map.rs | 4 + .../flowy-grid/src/services/block_manager.rs | 26 ++-- .../flowy-grid/src/services/grid_editor.rs | 56 +++++--- .../group/group_generator/checkbox_group.rs | 40 ++++-- .../group/group_generator/generator.rs | 112 +++++++++------- .../group_generator/select_option_group.rs | 68 +++++++--- .../src/services/group/group_service.rs | 122 ++++++++++-------- shared-lib/flowy-error-code/src/code.rs | 3 + .../src/revision/grid_rev.rs | 4 +- 13 files changed, 313 insertions(+), 168 deletions(-) create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index b712a701df..4d4fcc78c1 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -146,9 +146,9 @@ pub struct GridBlockChangesetPB { pub hide_rows: Vec, } impl GridBlockChangesetPB { - pub fn insert(block_id: &str, inserted_rows: Vec) -> Self { + pub fn insert(block_id: String, inserted_rows: Vec) -> Self { Self { - block_id: block_id.to_owned(), + block_id, inserted_rows, ..Default::default() } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs new file mode 100644 index 0000000000..d641c2d723 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs @@ -0,0 +1,29 @@ +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; +use flowy_grid_data_model::parser::NotEmptyStr; + +#[derive(ProtoBuf, Debug, Default, Clone)] +pub struct CreateBoardCardPayloadPB { + #[pb(index = 1)] + pub grid_id: String, + + #[pb(index = 2)] + pub group_id: String, +} +pub struct CreateBoardCardParams { + pub grid_id: String, + pub group_id: String, +} + +impl TryInto for CreateBoardCardPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; + Ok(CreateBoardCardParams { + grid_id: grid_id.0, + group_id: group_id.0, + }) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs index c1834c1009..7aa3a1a43e 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs @@ -1,5 +1,7 @@ +mod board_card; mod configuration; mod group; +pub use board_card::*; pub use configuration::*; pub use group::*; diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index e834c4652e..7f8ae72773 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -416,3 +416,14 @@ pub(crate) async fn get_groups_handler( let group = editor.load_groups().await?; data_result(group) } + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn create_board_card_handler( + data: Data, + manager: AppData>, +) -> DataResult { + let params: CreateBoardCardParams = data.into_inner().try_into()?; + let editor = manager.get_grid_editor(params.grid_id.as_ref())?; + let row = editor.create_board_card().await?; + data_result(row) +} diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 9e0b2ef5dd..9a5303220c 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -39,6 +39,7 @@ pub fn create(grid_manager: Arc) -> Module { // Date .event(GridEvent::UpdateDateCell, update_date_cell_handler) // Group + .event(GridEvent::CreateBoardCard, create_row_handler) .event(GridEvent::GetGroup, get_groups_handler); module @@ -209,4 +210,7 @@ pub enum GridEvent { #[event(input = "GridIdPB", output = "RepeatedGridGroupPB")] GetGroup = 100, + + #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")] + CreateBoardCard = 110, } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index afeb58c5d4..8da912534f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -62,22 +62,16 @@ impl GridBlockManager { Ok(self.get_editor(&block_id).await?) } - pub(crate) async fn create_row( - &self, - block_id: &str, - row_rev: RowRevision, - start_row_id: Option, - ) -> FlowyResult { + pub(crate) async fn create_row(&self, row_rev: RowRevision, start_row_id: Option) -> FlowyResult { + let block_id = row_rev.block_id.clone(); let _ = self.persistence.insert(&row_rev.block_id, &row_rev.id)?; let editor = self.get_editor(&row_rev.block_id).await?; let mut index_row_order = InsertedRowPB::from(&row_rev); let (row_count, row_index) = editor.create_row(row_rev, start_row_id).await?; index_row_order.index = row_index; - - let _ = self - .notify_did_update_block(block_id, GridBlockChangesetPB::insert(block_id, vec![index_row_order])) - .await?; + let changeset = GridBlockChangesetPB::insert(block_id.clone(), vec![index_row_order]); + let _ = self.notify_did_update_block(&block_id, changeset).await?; Ok(row_count) } @@ -98,10 +92,16 @@ impl GridBlockManager { row_order.index = index; inserted_row_orders.push(row_order); } - changesets.push(GridBlockMetaRevisionChangeset::from_row_count(&block_id, row_count)); + changesets.push(GridBlockMetaRevisionChangeset::from_row_count( + block_id.clone(), + row_count, + )); let _ = self - .notify_did_update_block(&block_id, GridBlockChangesetPB::insert(&block_id, inserted_row_orders)) + .notify_did_update_block( + &block_id, + GridBlockChangesetPB::insert(block_id.clone(), inserted_row_orders), + ) .await?; } @@ -154,7 +154,7 @@ impl GridBlockManager { .map(|row_info| Cow::Owned(row_info.row_id().to_owned())) .collect::>>(); let row_count = editor.delete_rows(row_ids).await?; - let changeset = GridBlockMetaRevisionChangeset::from_row_count(&grid_block.id, row_count); + let changeset = GridBlockMetaRevisionChangeset::from_row_count(grid_block.id.clone(), row_count); changesets.push(changeset); } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index f2e28701df..82c4b23afb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -37,7 +37,7 @@ pub struct GridRevisionEditor { pub(crate) filter_service: Arc, #[allow(dead_code)] - pub(crate) group_service: Arc, + pub(crate) group_service: Arc>, } impl Drop for GridRevisionEditor { @@ -62,17 +62,17 @@ impl GridRevisionEditor { let block_meta_revs = grid_pad.read().await.get_block_meta_revs(); let block_manager = Arc::new(GridBlockManager::new(grid_id, &user, block_meta_revs, persistence).await?); let filter_service = - Arc::new(GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await); + GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await; let group_service = - Arc::new(GridGroupService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await); + GridGroupService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await; let editor = Arc::new(Self { grid_id: grid_id.to_owned(), user, grid_pad, rev_manager, block_manager, - filter_service, - group_service, + filter_service: Arc::new(filter_service), + group_service: Arc::new(RwLock::new(group_service)), }); Ok(editor) @@ -275,20 +275,8 @@ impl GridRevisionEditor { } pub async fn create_row(&self, start_row_id: Option) -> FlowyResult { - let field_revs = self.grid_pad.read().await.get_field_revs(None)?; - let block_id = self.block_id().await?; - - // insert empty row below the row whose id is upper_row_id - let row_rev = RowRevisionBuilder::new(&block_id, &field_revs).build(); - let row_order = RowPB::from(&row_rev); - - // insert the row - let row_count = self.block_manager.create_row(&block_id, row_rev, start_row_id).await?; - - // update block row count - let changeset = GridBlockMetaRevisionChangeset::from_row_count(&block_id, row_count); - let _ = self.update_block(changeset).await?; - Ok(row_order) + let row_rev = self.create_row_rev().await?; + self.create_row_pb(row_rev, start_row_id).await } pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { @@ -564,12 +552,40 @@ impl GridRevisionEditor { }) } + pub async fn create_board_card(&self) -> FlowyResult { + let mut row_rev = self.create_row_rev().await?; + let _ = self.group_service.write().await.create_board_card(&mut row_rev).await; + self.create_row_pb(row_rev, None).await + } + #[tracing::instrument(level = "trace", skip_all, err)] pub async fn load_groups(&self) -> FlowyResult { - let groups = self.group_service.load_groups().await.unwrap_or_default(); + let groups = self.group_service.write().await.load_groups().await.unwrap_or_default(); Ok(RepeatedGridGroupPB { items: groups }) } + async fn create_row_rev(&self) -> FlowyResult { + let field_revs = self.grid_pad.read().await.get_field_revs(None)?; + let block_id = self.block_id().await?; + + // insert empty row below the row whose id is upper_row_id + let row_rev = RowRevisionBuilder::new(&block_id, &field_revs).build(); + Ok(row_rev) + } + + async fn create_row_pb(&self, row_rev: RowRevision, start_row_id: Option) -> FlowyResult { + let row_pb = RowPB::from(&row_rev); + let block_id = row_rev.block_id.clone(); + + // insert the row + let row_count = self.block_manager.create_row(row_rev, start_row_id).await?; + + // update block row count + let changeset = GridBlockMetaRevisionChangeset::from_row_count(block_id, row_count); + let _ = self.update_block(changeset).await?; + Ok(row_pb) + } + async fn modify(&self, f: F) -> FlowyResult<()> where F: for<'a> FnOnce(&'a mut GridRevisionPad) -> FlowyResult>, diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs index 32b699d3a1..b976310bed 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -1,17 +1,43 @@ -use crate::entities::CheckboxGroupConfigurationPB; +use crate::entities::{CheckboxGroupConfigurationPB, RowPB}; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::RowRevision; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; -use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator}; +use crate::services::group::{ + Group, GroupActionHandler, GroupCellContentProvider, GroupController, GroupGenerator, Groupable, +}; pub type CheckboxGroupController = GroupController; +impl Groupable for CheckboxGroupController { + type CellDataType = CheckboxCellData; + + fn can_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool { + false + } +} + +impl GroupActionHandler for CheckboxGroupController { + fn get_groups(&self) -> Vec { + self.groups() + } + + fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()> { + self.handle_row(row_rev) + } + + fn create_card(&self, row_rev: &mut RowRevision) { + todo!() + } +} + pub struct CheckboxGroupGenerator(); impl GroupGenerator for CheckboxGroupGenerator { type ConfigurationType = CheckboxGroupConfigurationPB; type TypeOptionType = CheckboxTypeOptionPB; - fn gen_groups( + fn generate_groups( _configuration: &Option, _type_option: &Option, _cell_content_provider: &dyn GroupCellContentProvider, @@ -33,11 +59,3 @@ impl GroupGenerator for CheckboxGroupGenerator { vec![check_group, uncheck_group] } } - -impl GroupAction for CheckboxGroupController { - type CellDataType = CheckboxCellData; - - fn should_group(&self, _content: &str, _cell_data: &Self::CellDataType) -> bool { - false - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index 1682bd9fbb..b841e745e4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -9,12 +9,6 @@ use indexmap::IndexMap; use std::marker::PhantomData; use std::sync::Arc; -pub trait GroupAction { - type CellDataType; - - fn should_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; -} - pub trait GroupCellContentProvider { /// We need to group the rows base on the deduplication cell content when the field type is /// RichText. @@ -27,25 +21,47 @@ pub trait GroupGenerator { type ConfigurationType; type TypeOptionType; - fn gen_groups( + fn generate_groups( configuration: &Option, type_option: &Option, cell_content_provider: &dyn GroupCellContentProvider, ) -> Vec; } +pub trait Groupable { + type CellDataType; + fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool; +} + +pub trait GroupActionHandler: Send + Sync { + fn get_groups(&self) -> Vec; + fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()>; + fn group_rows(&mut self, row_revs: &[Arc]) -> FlowyResult<()> { + for row_rev in row_revs { + let _ = self.group_row(row_rev)?; + } + Ok(()) + } + fn create_card(&self, row_rev: &mut RowRevision); +} + const DEFAULT_GROUP_ID: &str = "default_group"; -pub struct GroupController { +/// C: represents the group configuration structure +/// T: the type option data deserializer that impl [TypeOptionDataDeserializer] +/// G: the group container generator +/// P: the parser that impl [CellBytesParser] for the CellBytes +pub struct GroupController { pub field_rev: Arc, - pub groups: IndexMap, - pub default_group: Group, + groups: IndexMap, + default_group: Group, pub type_option: Option, pub configuration: Option, group_action_phantom: PhantomData, - cell_parser_phantom: PhantomData, + cell_parser_phantom: PhantomData

, } +#[derive(Clone)] pub struct Group { pub id: String, pub desc: String, @@ -63,7 +79,7 @@ impl std::convert::From for GroupPB { } } -impl GroupController +impl GroupController where C: TryFrom, T: TypeOptionDataDeserializer, @@ -80,7 +96,7 @@ where }; let field_type_rev = field_rev.field_type_rev; let type_option = field_rev.get_type_option_entry::(field_type_rev); - let groups = G::gen_groups(&configuration, &type_option, cell_content_provider); + let groups = G::generate_groups(&configuration, &type_option, cell_content_provider); let default_group = Group { id: DEFAULT_GROUP_ID.to_owned(), @@ -100,9 +116,9 @@ where }) } - pub fn take_groups(self) -> Vec { - let default_group = self.default_group; - let mut groups: Vec = self.groups.into_values().collect(); + pub fn groups(&self) -> Vec { + let default_group = self.default_group.clone(); + let mut groups: Vec = self.groups.values().cloned().collect(); if !default_group.rows.is_empty() { groups.push(default_group); } @@ -110,48 +126,50 @@ where } } -impl GroupController +impl GroupController where - CP: CellBytesParser, - Self: GroupAction, + P: CellBytesParser, + Self: Groupable, { - pub fn group_rows(&mut self, rows: &[Arc]) -> FlowyResult<()> { + pub fn handle_row(&mut self, row: &RowRevision) -> FlowyResult<()> { if self.configuration.is_none() { return Ok(()); } - tracing::debug!("group {} rows", rows.len()); - - for row in rows { - if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { - let mut records: Vec = vec![]; - - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &self.field_rev); - let cell_data = cell_bytes.parser::()?; - for group in self.groups.values() { - if self.should_group(&group.content, &cell_data) { - records.push(GroupRecord { - row: row.into(), - group_id: group.id.clone(), - }); - } + if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { + let mut records: Vec = vec![]; + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &self.field_rev); + let cell_data = cell_bytes.parser::

()?; + for group in self.groups.values() { + if self.can_group(&group.content, &cell_data) { + records.push(GroupRecord { + row: row.into(), + group_id: group.id.clone(), + }); } - - if records.is_empty() { - self.default_group.rows.push(row.into()); - } else { - for record in records { - if let Some(group) = self.groups.get_mut(&record.group_id) { - group.rows.push(record.row); - } - } - } - } else { - self.default_group.rows.push(row.into()); } + + if records.is_empty() { + self.default_group.rows.push(row.into()); + } else { + for record in records { + if let Some(group) = self.groups.get_mut(&record.group_id) { + group.rows.push(record.row); + } + } + } + } else { + self.default_group.rows.push(row.into()); } Ok(()) } + + pub fn group_rows(&mut self, rows: &[Arc]) -> FlowyResult<()> { + for row in rows { + let _ = self.handle_row(row)?; + } + Ok(()) + } } struct GroupRecord { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index bc4f1c4709..6666808ee5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -1,9 +1,13 @@ -use crate::entities::SelectOptionGroupConfigurationPB; +use crate::entities::{RowPB, SelectOptionGroupConfigurationPB}; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::RowRevision; use crate::services::field::{ MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB, }; -use crate::services::group::{Group, GroupAction, GroupCellContentProvider, GroupController, GroupGenerator}; +use crate::services::group::{ + Group, GroupActionHandler, GroupCellContentProvider, GroupController, GroupGenerator, Groupable, +}; // SingleSelect pub type SingleSelectGroupController = GroupController< @@ -13,11 +17,32 @@ pub type SingleSelectGroupController = GroupController< SelectOptionCellDataParser, >; +impl Groupable for SingleSelectGroupController { + type CellDataType = SelectOptionCellDataPB; + fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { + cell_data.select_options.iter().any(|option| option.id == content) + } +} + +impl GroupActionHandler for SingleSelectGroupController { + fn get_groups(&self) -> Vec { + self.groups() + } + + fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()> { + self.handle_row(row_rev) + } + + fn create_card(&self, row_rev: &mut RowRevision) { + todo!() + } +} + pub struct SingleSelectGroupGenerator(); impl GroupGenerator for SingleSelectGroupGenerator { type ConfigurationType = SelectOptionGroupConfigurationPB; type TypeOptionType = SingleSelectTypeOptionPB; - fn gen_groups( + fn generate_groups( _configuration: &Option, type_option: &Option, _cell_content_provider: &dyn GroupCellContentProvider, @@ -38,13 +63,6 @@ impl GroupGenerator for SingleSelectGroupGenerator { } } -impl GroupAction for SingleSelectGroupController { - type CellDataType = SelectOptionCellDataPB; - fn should_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { - cell_data.select_options.iter().any(|option| option.id == content) - } -} - // MultiSelect pub type MultiSelectGroupController = GroupController< SelectOptionGroupConfigurationPB, @@ -53,12 +71,33 @@ pub type MultiSelectGroupController = GroupController< SelectOptionCellDataParser, >; +impl Groupable for MultiSelectGroupController { + type CellDataType = SelectOptionCellDataPB; + fn can_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { + cell_data.select_options.iter().any(|option| option.id == content) + } +} + +impl GroupActionHandler for MultiSelectGroupController { + fn get_groups(&self) -> Vec { + self.groups() + } + + fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()> { + self.handle_row(row_rev) + } + + fn create_card(&self, row_rev: &mut RowRevision) { + todo!() + } +} + pub struct MultiSelectGroupGenerator(); impl GroupGenerator for MultiSelectGroupGenerator { type ConfigurationType = SelectOptionGroupConfigurationPB; type TypeOptionType = MultiSelectTypeOptionPB; - fn gen_groups( + fn generate_groups( _configuration: &Option, type_option: &Option, _cell_content_provider: &dyn GroupCellContentProvider, @@ -78,10 +117,3 @@ impl GroupGenerator for MultiSelectGroupGenerator { } } } - -impl GroupAction for MultiSelectGroupController { - type CellDataType = SelectOptionCellDataPB; - fn should_group(&self, content: &str, cell_data: &SelectOptionCellDataPB) -> bool { - cell_data.select_options.iter().any(|option| option.id == content) - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 5be0141ca9..573ffcea75 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,12 +1,14 @@ use crate::services::block_manager::GridBlockManager; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::group::{ - CheckboxGroupController, Group, GroupCellContentProvider, MultiSelectGroupController, SingleSelectGroupController, + CheckboxGroupController, Group, GroupActionHandler, GroupCellContentProvider, MultiSelectGroupController, + SingleSelectGroupController, }; use crate::entities::{ - CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, NumberGroupConfigurationPB, - SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, + CheckboxGroupConfigurationPB, CreateBoardCardParams, DateGroupConfigurationPB, FieldType, GroupPB, + NumberGroupConfigurationPB, RowPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, + UrlGroupConfigurationPB, }; use bytes::Bytes; use flowy_error::FlowyResult; @@ -18,10 +20,9 @@ use tokio::sync::RwLock; pub(crate) struct GridGroupService { #[allow(dead_code)] scheduler: Arc, - #[allow(dead_code)] grid_pad: Arc>, - #[allow(dead_code)] block_manager: Arc, + group_action_handler: Option>>, } impl GridGroupService { @@ -35,14 +36,14 @@ impl GridGroupService { scheduler, grid_pad, block_manager, + group_action_handler: None, } } - pub(crate) async fn load_groups(&self) -> Option> { - let grid_pad = self.grid_pad.read().await; - let field_rev = find_group_field(grid_pad.fields()).unwrap(); + pub(crate) async fn load_groups(&mut self) -> Option> { + let field_rev = find_group_field(self.grid_pad.read().await.fields()).unwrap(); let field_type: FieldType = field_rev.field_type_rev.into(); - let configuration = self.get_group_configuration(field_rev).await; + let configuration = self.get_group_configuration(&field_rev).await; let blocks = self.block_manager.get_block_snapshots(None).await.unwrap(); let row_revs = blocks @@ -51,19 +52,28 @@ impl GridGroupService { .flatten() .collect::>>(); - match self.build_groups(&field_type, field_rev, row_revs, configuration) { + match self + .build_groups(&field_type, &field_rev, row_revs, configuration) + .await + { Ok(groups) => Some(groups), Err(_) => None, } } - async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { + pub(crate) async fn create_board_card(&self, row_rev: &mut RowRevision) { + if let Some(group_action_handler) = self.group_action_handler.as_ref() { + group_action_handler.write().await.create_card(row_rev); + } + } + + pub(crate) async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { let grid_pad = self.grid_pad.read().await; let setting = grid_pad.get_setting_rev(); let layout = &setting.layout; let configurations = setting.get_groups(layout, &field_rev.id, &field_rev.field_type_rev); match configurations { - None => self.default_group_configuration(field_rev), + None => default_group_configuration(field_rev), Some(mut configurations) => { assert_eq!(configurations.len(), 1); (&*configurations.pop().unwrap()).clone() @@ -71,79 +81,81 @@ impl GridGroupService { } } - fn default_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { - let field_type: FieldType = field_rev.field_type_rev.clone().into(); - let bytes: Bytes = match field_type { - FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(), - FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(), - FieldType::DateTime => DateGroupConfigurationPB::default().try_into().unwrap(), - FieldType::SingleSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), - FieldType::MultiSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), - FieldType::Checkbox => CheckboxGroupConfigurationPB::default().try_into().unwrap(), - FieldType::URL => UrlGroupConfigurationPB::default().try_into().unwrap(), - }; - GroupConfigurationRevision { - id: gen_grid_group_id(), - field_id: field_rev.id.clone(), - field_type_rev: field_rev.field_type_rev.clone(), - content: Some(bytes.to_vec()), - } - } - #[tracing::instrument(level = "trace", skip_all, err)] - fn build_groups( - &self, + async fn build_groups( + &mut self, field_type: &FieldType, field_rev: &Arc, row_revs: Vec>, configuration: GroupConfigurationRevision, ) -> FlowyResult> { - let groups: Vec = match field_type { + match field_type { FieldType::RichText => { // let generator = GroupGenerator::::from_configuration(configuration); - vec![] } FieldType::Number => { // let generator = GroupGenerator::::from_configuration(configuration); - vec![] } FieldType::DateTime => { // let generator = GroupGenerator::::from_configuration(configuration); - vec![] } FieldType::SingleSelect => { - let mut group_controller = - SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; - let _ = group_controller.group_rows(&row_revs)?; - group_controller.take_groups() + let controller = SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + self.group_action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::MultiSelect => { - let mut group_controller = - MultiSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; - let _ = group_controller.group_rows(&row_revs)?; - group_controller.take_groups() + let controller = MultiSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + self.group_action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::Checkbox => { - let mut group_controller = - CheckboxGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; - let _ = group_controller.group_rows(&row_revs)?; - group_controller.take_groups() + let controller = CheckboxGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + self.group_action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::URL => { // let generator = GroupGenerator::::from_configuration(configuration); - vec![] } }; + let mut groups = vec![]; + if let Some(group_action_handler) = self.group_action_handler.as_ref() { + let mut write_guard = group_action_handler.write().await; + let _ = write_guard.group_rows(&row_revs)?; + groups = write_guard.get_groups(); + drop(write_guard); + } + Ok(groups.into_iter().map(GroupPB::from).collect()) } } -fn find_group_field(field_revs: &[Arc]) -> Option<&Arc> { - field_revs.iter().find(|field_rev| { - let field_type: FieldType = field_rev.field_type_rev.into(); - field_type.can_be_group() - }) +fn find_group_field(field_revs: &[Arc]) -> Option> { + let field_rev = field_revs + .iter() + .find(|field_rev| { + let field_type: FieldType = field_rev.field_type_rev.into(); + field_type.can_be_group() + }) + .cloned(); + field_rev } impl GroupCellContentProvider for Arc> {} + +fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision { + let field_type: FieldType = field_rev.field_type_rev.clone().into(); + let bytes: Bytes = match field_type { + FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(), + FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(), + FieldType::DateTime => DateGroupConfigurationPB::default().try_into().unwrap(), + FieldType::SingleSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), + FieldType::MultiSelect => SelectOptionGroupConfigurationPB::default().try_into().unwrap(), + FieldType::Checkbox => CheckboxGroupConfigurationPB::default().try_into().unwrap(), + FieldType::URL => UrlGroupConfigurationPB::default().try_into().unwrap(), + }; + GroupConfigurationRevision { + id: gen_grid_group_id(), + field_id: field_rev.id.clone(), + field_type_rev: field_rev.field_type_rev.clone(), + content: Some(bytes.to_vec()), + } +} diff --git a/shared-lib/flowy-error-code/src/code.rs b/shared-lib/flowy-error-code/src/code.rs index 352c4abbdb..5b676073da 100644 --- a/shared-lib/flowy-error-code/src/code.rs +++ b/shared-lib/flowy-error-code/src/code.rs @@ -111,6 +111,9 @@ pub enum ErrorCode { #[display(fmt = "Field's type option data should not be empty")] TypeOptionDataIsEmpty = 450, + #[display(fmt = "Group id is empty")] + GroupIdIsEmpty = 460, + #[display(fmt = "Invalid date time format")] InvalidDateTimeFormat = 500, diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index b60baff811..184f5266ed 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -88,9 +88,9 @@ pub struct GridBlockMetaRevisionChangeset { } impl GridBlockMetaRevisionChangeset { - pub fn from_row_count(block_id: &str, row_count: i32) -> Self { + pub fn from_row_count(block_id: String, row_count: i32) -> Self { Self { - block_id: block_id.to_string(), + block_id, start_row_index: None, row_count: Some(row_count), } From 43eaa2748d45532d94765581427c670df6e1028b Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 14 Aug 2022 11:05:55 +0800 Subject: [PATCH 115/224] chore: insert cell content when creating card --- .../plugins/board/application/board_bloc.dart | 6 +- .../application/board_data_controller.dart | 8 +- .../board/presentation/board_page.dart | 2 +- .../application/grid_data_controller.dart | 4 +- .../grid/application/grid_service.dart | 12 ++- .../src/entities/group_entities/board_card.rs | 41 ++++++++++ .../rust-lib/flowy-grid/src/event_handler.rs | 2 +- frontend/rust-lib/flowy-grid/src/event_map.rs | 2 +- .../src/services/cell/cell_operation.rs | 41 ++++++++++ .../flowy-grid/src/services/grid_editor.rs | 11 ++- .../group/group_generator/checkbox_group.rs | 15 ++-- .../group/group_generator/generator.rs | 81 ++++++++++--------- .../group_generator/select_option_group.rs | 49 ++++++++--- .../src/services/group/group_service.rs | 28 +++++-- .../src/services/row/row_builder.rs | 76 +++++++++++------ frontend/rust-lib/flowy-grid/src/util.rs | 13 +-- .../flowy-grid/tests/grid/block_test/util.rs | 11 +-- .../src/client_grid/grid_revision_pad.rs | 2 +- 18 files changed, 286 insertions(+), 118 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 1db180d4e4..a4a892a7fc 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -52,8 +52,8 @@ class BoardBloc extends Bloc { _startListening(); await _loadGrid(emit); }, - createRow: () async { - final result = await _dataController.createRow(); + createRow: (groupId) async { + final result = await _dataController.createBoardCard(groupId); result.fold( (rowPB) { emit(state.copyWith(editingRow: some(rowPB))); @@ -153,7 +153,7 @@ class BoardBloc extends Bloc { @freezed class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = InitialGrid; - const factory BoardEvent.createRow() = _CreateRow; + const factory BoardEvent.createRow(String groupId) = _CreateRow; const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; const factory BoardEvent.didReceiveGroups(List groups) = _DidReceiveGroup; diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 619d16f99e..e4b4f90520 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -21,7 +21,7 @@ typedef OnError = void Function(FlowyError); class BoardDataController { final String gridId; - final GridService _gridFFIService; + final GridFFIService _gridFFIService; final GridFieldCache fieldCache; // key: the block id @@ -45,7 +45,7 @@ class BoardDataController { BoardDataController({required ViewPB view}) : gridId = view.id, _blocks = LinkedHashMap.new(), - _gridFFIService = GridService(gridId: view.id), + _gridFFIService = GridFFIService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); void addListener({ @@ -88,8 +88,8 @@ class BoardDataController { ); } - Future> createRow() { - return _gridFFIService.createRow(); + Future> createBoardCard(String groupId) { + return _gridFFIService.createBoardCard(groupId); } Future dispose() async { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index bf04c22f57..faf7e193b0 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -88,7 +88,7 @@ class BoardContent extends StatelessWidget { height: 50, margin: config.columnItemPadding, onAddButtonClick: () { - context.read().add(const BoardEvent.createRow()); + context.read().add(BoardEvent.createRow(columnData.id)); }); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 9de6c65997..f11db25167 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -24,7 +24,7 @@ typedef ListenOnRowChangedCondition = bool Function(); class GridDataController { final String gridId; - final GridService _gridFFIService; + final GridFFIService _gridFFIService; final GridFieldCache fieldCache; // key: the block id @@ -47,7 +47,7 @@ class GridDataController { GridDataController({required ViewPB view}) : gridId = view.id, _blocks = LinkedHashMap.new(), - _gridFFIService = GridService(gridId: view.id), + _gridFFIService = GridFFIService(gridId: view.id), fieldCache = GridFieldCache(gridId: view.id); void addListener({ diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index 093782d32f..4315fff38b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -3,14 +3,15 @@ import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/board_card.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; -class GridService { +class GridFFIService { final String gridId; - GridService({ + GridFFIService({ required this.gridId, }); @@ -27,6 +28,13 @@ class GridService { return GridEventCreateRow(payload).send(); } + Future> createBoardCard(String groupId) { + CreateBoardCardPayloadPB payload = CreateBoardCardPayloadPB.create() + ..gridId = gridId + ..groupId = groupId; + return GridEventCreateBoardCard(payload).send(); + } + Future> getFields( {required List fieldIds}) { final payload = QueryFieldPayloadPB.create() diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs index d641c2d723..e2dac9069d 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs @@ -1,3 +1,4 @@ +use crate::entities::RowPB; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -27,3 +28,43 @@ impl TryInto for CreateBoardCardPayloadPB { }) } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct BoardCardChangesetPB { + #[pb(index = 1)] + pub group_id: String, + + #[pb(index = 2)] + pub inserted_cards: Vec, + + #[pb(index = 3)] + pub deleted_cards: Vec, + + #[pb(index = 4)] + pub updated_cards: Vec, +} +impl BoardCardChangesetPB { + pub fn insert(group_id: String, inserted_cards: Vec) -> Self { + Self { + group_id, + inserted_cards, + ..Default::default() + } + } + + pub fn delete(group_id: String, deleted_cards: Vec) -> Self { + Self { + group_id, + deleted_cards, + ..Default::default() + } + } + + pub fn update(group_id: String, updated_cards: Vec) -> Self { + Self { + group_id, + updated_cards, + ..Default::default() + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 7f8ae72773..fa23e1f4cf 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -424,6 +424,6 @@ pub(crate) async fn create_board_card_handler( ) -> DataResult { let params: CreateBoardCardParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(params.grid_id.as_ref())?; - let row = editor.create_board_card().await?; + let row = editor.create_board_card(¶ms.group_id).await?; data_result(row) } diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 9a5303220c..980087fee0 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -39,7 +39,7 @@ pub fn create(grid_manager: Arc) -> Module { // Date .event(GridEvent::UpdateDateCell, update_date_cell_handler) // Group - .event(GridEvent::CreateBoardCard, create_row_handler) + .event(GridEvent::CreateBoardCard, create_board_card_handler) .event(GridEvent::GetGroup, get_groups_handler); module diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 4a12bfd613..cb84a4f8f4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -135,6 +135,47 @@ pub fn try_decode_cell_data( } } +pub fn insert_text_cell(s: String, field_rev: &FieldRevision) -> CellRevision { + let data = apply_cell_data_changeset(s, None, field_rev).unwrap(); + CellRevision::new(data) +} + +pub fn insert_number_cell(num: i64, field_rev: &FieldRevision) -> CellRevision { + let data = apply_cell_data_changeset(num, None, field_rev).unwrap(); + CellRevision::new(data) +} + +pub fn insert_url_cell(url: String, field_rev: &FieldRevision) -> CellRevision { + let data = apply_cell_data_changeset(url, None, field_rev).unwrap(); + CellRevision::new(data) +} + +pub fn insert_checkbox_cell(is_check: bool, field_rev: &FieldRevision) -> CellRevision { + let s = if is_check { + CHECK.to_string() + } else { + UNCHECK.to_string() + }; + let data = apply_cell_data_changeset(s, None, field_rev).unwrap(); + CellRevision::new(data) +} + +pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevision { + let cell_data = serde_json::to_string(&DateCellChangesetPB { + date: Some(timestamp.to_string()), + time: None, + }) + .unwrap(); + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); + CellRevision::new(data) +} + +pub fn insert_select_option_cell(option_id: String, field_rev: &FieldRevision) -> CellRevision { + let cell_data = SelectOptionCellChangeset::from_insert(&option_id).to_str(); + let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); + CellRevision::new(data) +} + /// If the cell data is not String type, it should impl this trait. /// Deserialize the String into cell specific data type. pub trait FromCellString { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 82c4b23afb..53c9a681bd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -552,9 +552,15 @@ impl GridRevisionEditor { }) } - pub async fn create_board_card(&self) -> FlowyResult { + pub async fn create_board_card(&self, group_id: &str) -> FlowyResult { let mut row_rev = self.create_row_rev().await?; - let _ = self.group_service.write().await.create_board_card(&mut row_rev).await; + let _ = self + .group_service + .write() + .await + .create_board_card(&mut row_rev, group_id) + .await; + self.create_row_pb(row_rev, None).await } @@ -573,6 +579,7 @@ impl GridRevisionEditor { Ok(row_rev) } + #[tracing::instrument(level = "trace", skip_all, err)] async fn create_row_pb(&self, row_rev: RowRevision, start_row_id: Option) -> FlowyResult { let row_pb = RowPB::from(&row_rev); let block_id = row_rev.block_id.clone(); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs index b976310bed..acc8cf8715 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -1,6 +1,7 @@ use crate::entities::{CheckboxGroupConfigurationPB, RowPB}; use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::RowRevision; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; +use std::sync::Arc; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; use crate::services::group::{ @@ -19,15 +20,19 @@ impl Groupable for CheckboxGroupController { } impl GroupActionHandler for CheckboxGroupController { + fn field_id(&self) -> &str { + &self.field_id + } + fn get_groups(&self) -> Vec { - self.groups() + self.make_groups() } - fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()> { - self.handle_row(row_rev) + fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()> { + self.handle_rows(row_revs, field_rev) } - fn create_card(&self, row_rev: &mut RowRevision) { + fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { todo!() } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index b841e745e4..856c026a80 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -1,11 +1,13 @@ use crate::entities::{GroupPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use bytes::Bytes; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{ FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer, }; +use futures::future::BoxFuture; use indexmap::IndexMap; +use lib_infra::future::{BoxResultFuture, FutureResult}; use std::marker::PhantomData; use std::sync::Arc; @@ -34,15 +36,14 @@ pub trait Groupable { } pub trait GroupActionHandler: Send + Sync { + fn field_id(&self) -> &str; fn get_groups(&self) -> Vec; - fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()>; - fn group_rows(&mut self, row_revs: &[Arc]) -> FlowyResult<()> { - for row_rev in row_revs { - let _ = self.group_row(row_rev)?; - } - Ok(()) - } - fn create_card(&self, row_rev: &mut RowRevision); + fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()>; + fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); +} + +pub trait GroupActionHandler2: Send + Sync { + fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); } const DEFAULT_GROUP_ID: &str = "default_group"; @@ -52,8 +53,8 @@ const DEFAULT_GROUP_ID: &str = "default_group"; /// G: the group container generator /// P: the parser that impl [CellBytesParser] for the CellBytes pub struct GroupController { - pub field_rev: Arc, - groups: IndexMap, + pub field_id: String, + pub groups_map: IndexMap, default_group: Group, pub type_option: Option, pub configuration: Option, @@ -86,7 +87,7 @@ where G: GroupGenerator, { pub fn new( - field_rev: Arc, + field_rev: &Arc, configuration: GroupConfigurationRevision, cell_content_provider: &dyn GroupCellContentProvider, ) -> FlowyResult { @@ -106,8 +107,8 @@ where }; Ok(Self { - field_rev, - groups: groups.into_iter().map(|group| (group.id.clone(), group)).collect(), + field_id: field_rev.id.clone(), + groups_map: groups.into_iter().map(|group| (group.id.clone(), group)).collect(), default_group, type_option, configuration, @@ -116,9 +117,9 @@ where }) } - pub fn groups(&self) -> Vec { + pub fn make_groups(&self) -> Vec { let default_group = self.default_group.clone(); - let mut groups: Vec = self.groups.values().cloned().collect(); + let mut groups: Vec = self.groups_map.values().cloned().collect(); if !default_group.rows.is_empty() { groups.push(default_group); } @@ -131,34 +132,38 @@ where P: CellBytesParser, Self: Groupable, { - pub fn handle_row(&mut self, row: &RowRevision) -> FlowyResult<()> { + pub fn handle_rows(&mut self, rows: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()> { + // The field_rev might be None if corresponding field_rev is deleted. if self.configuration.is_none() { return Ok(()); } - if let Some(cell_rev) = row.cells.get(&self.field_rev.id) { - let mut records: Vec = vec![]; - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &self.field_rev); - let cell_data = cell_bytes.parser::

()?; - for group in self.groups.values() { - if self.can_group(&group.content, &cell_data) { - records.push(GroupRecord { - row: row.into(), - group_id: group.id.clone(), - }); - } - } - if records.is_empty() { - self.default_group.rows.push(row.into()); - } else { - for record in records { - if let Some(group) = self.groups.get_mut(&record.group_id) { - group.rows.push(record.row); + for row in rows { + if let Some(cell_rev) = row.cells.get(&self.field_id) { + let mut records: Vec = vec![]; + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &field_rev); + let cell_data = cell_bytes.parser::

()?; + for group in self.groups_map.values() { + if self.can_group(&group.content, &cell_data) { + records.push(GroupRecord { + row: row.into(), + group_id: group.id.clone(), + }); } } + + if records.is_empty() { + self.default_group.rows.push(row.into()); + } else { + for record in records { + if let Some(group) = self.groups_map.get_mut(&record.group_id) { + group.rows.push(record.row); + } + } + } + } else { + self.default_group.rows.push(row.into()); } - } else { - self.default_group.rows.push(row.into()); } Ok(()) @@ -166,7 +171,7 @@ where pub fn group_rows(&mut self, rows: &[Arc]) -> FlowyResult<()> { for row in rows { - let _ = self.handle_row(row)?; + // let _ = self.handle_row(row)?; } Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index 6666808ee5..a3a133b24c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -1,6 +1,9 @@ use crate::entities::{RowPB, SelectOptionGroupConfigurationPB}; -use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::RowRevision; +use crate::services::cell::insert_select_option_cell; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; +use lib_infra::future::FutureResult; +use std::sync::Arc; use crate::services::field::{ MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB, @@ -25,16 +28,27 @@ impl Groupable for SingleSelectGroupController { } impl GroupActionHandler for SingleSelectGroupController { + fn field_id(&self) -> &str { + &self.field_id + } + fn get_groups(&self) -> Vec { - self.groups() + self.make_groups() } - fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()> { - self.handle_row(row_rev) + fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()> { + self.handle_rows(row_revs, field_rev) } - fn create_card(&self, row_rev: &mut RowRevision) { - todo!() + fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + let group: Option<&Group> = self.groups_map.get(group_id); + match group { + None => {} + Some(group) => { + let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); + row_rev.cells.insert(field_rev.id.clone(), cell_rev); + } + } } } @@ -79,16 +93,27 @@ impl Groupable for MultiSelectGroupController { } impl GroupActionHandler for MultiSelectGroupController { + fn field_id(&self) -> &str { + &self.field_id + } + fn get_groups(&self) -> Vec { - self.groups() + self.make_groups() } - fn group_row(&mut self, row_rev: &RowRevision) -> FlowyResult<()> { - self.handle_row(row_rev) + fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()> { + self.handle_rows(row_revs, field_rev) } - fn create_card(&self, row_rev: &mut RowRevision) { - todo!() + fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + let group: Option<&Group> = self.groups_map.get(group_id); + match group { + None => tracing::warn!("Can not find the group: {}", group_id), + Some(group) => { + let cell_rev = insert_select_option_cell(group.id.clone(), field_rev); + row_rev.cells.insert(field_rev.id.clone(), cell_rev); + } + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 573ffcea75..fa5f805b7f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -14,6 +14,7 @@ use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision}; use flowy_sync::client_grid::GridRevisionPad; +use futures::future::BoxFuture; use std::sync::Arc; use tokio::sync::RwLock; @@ -61,9 +62,24 @@ impl GridGroupService { } } - pub(crate) async fn create_board_card(&self, row_rev: &mut RowRevision) { + #[tracing::instrument(level = "debug", skip(self, row_rev))] + pub(crate) async fn create_board_card(&self, row_rev: &mut RowRevision, group_id: &str) { if let Some(group_action_handler) = self.group_action_handler.as_ref() { - group_action_handler.write().await.create_card(row_rev); + match self + .grid_pad + .read() + .await + .get_field_rev(group_action_handler.read().await.field_id()) + { + None => tracing::warn!("Fail to create card because the field does not exist"), + Some((_, field_rev)) => { + tracing::trace!("Create card"); + group_action_handler + .write() + .await + .create_card(row_rev, field_rev, group_id); + } + } } } @@ -100,15 +116,15 @@ impl GridGroupService { // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::SingleSelect => { - let controller = SingleSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + let controller = SingleSelectGroupController::new(field_rev, configuration, &self.grid_pad)?; self.group_action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::MultiSelect => { - let controller = MultiSelectGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + let controller = MultiSelectGroupController::new(field_rev, configuration, &self.grid_pad)?; self.group_action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::Checkbox => { - let controller = CheckboxGroupController::new(field_rev.clone(), configuration, &self.grid_pad)?; + let controller = CheckboxGroupController::new(field_rev, configuration, &self.grid_pad)?; self.group_action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::URL => { @@ -119,7 +135,7 @@ impl GridGroupService { let mut groups = vec![]; if let Some(group_action_handler) = self.group_action_handler.as_ref() { let mut write_guard = group_action_handler.write().await; - let _ = write_guard.group_rows(&row_revs)?; + let _ = write_guard.group_rows(&row_revs, field_rev)?; groups = write_guard.get_groups(); drop(write_guard); } diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index 3db4c0b550..016bbea128 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -1,4 +1,7 @@ -use crate::services::cell::apply_cell_data_changeset; +use crate::services::cell::{ + apply_cell_data_changeset, insert_checkbox_cell, insert_date_cell, insert_number_cell, insert_select_option_cell, + insert_text_cell, insert_url_cell, +}; use crate::services::field::{DateCellChangesetPB, SelectOptionCellChangeset}; use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; @@ -34,47 +37,68 @@ impl<'a> RowRevisionBuilder<'a> { } } - pub fn insert_cell(&mut self, field_id: &str, data: String) { + pub fn insert_text_cell(&mut self, field_id: &str, data: String) { match self.field_rev_map.get(&field_id.to_owned()) { - None => { - tracing::warn!("Can't find the field with id: {}", field_id); - } + None => tracing::warn!("Can't find the text field with id: {}", field_id), Some(field_rev) => { - let data = apply_cell_data_changeset(data, None, field_rev).unwrap(); - let cell = CellRevision::new(data); - self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); + self.payload + .cell_by_field_id + .insert(field_id.to_owned(), insert_text_cell(data, field_rev)); + } + } + } + + pub fn insert_url_cell(&mut self, field_id: &str, data: String) { + match self.field_rev_map.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the url field with id: {}", field_id), + Some(field_rev) => { + self.payload + .cell_by_field_id + .insert(field_id.to_owned(), insert_url_cell(data, field_rev)); + } + } + } + + pub fn insert_number_cell(&mut self, field_id: &str, num: i64) { + match self.field_rev_map.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the number field with id: {}", field_id), + Some(field_rev) => { + self.payload + .cell_by_field_id + .insert(field_id.to_owned(), insert_number_cell(num, field_rev)); + } + } + } + + pub fn insert_checkbox_cell(&mut self, field_id: &str, is_check: bool) { + match self.field_rev_map.get(&field_id.to_owned()) { + None => tracing::warn!("Can't find the checkbox field with id: {}", field_id), + Some(field_rev) => { + self.payload + .cell_by_field_id + .insert(field_id.to_owned(), insert_checkbox_cell(is_check, field_rev)); } } } pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64) { match self.field_rev_map.get(&field_id.to_owned()) { - None => { - tracing::warn!("Invalid field_id: {}", field_id); - } + None => tracing::warn!("Can't find the date field with id: {}", field_id), Some(field_rev) => { - let cell_data = serde_json::to_string(&DateCellChangesetPB { - date: Some(timestamp.to_string()), - time: None, - }) - .unwrap(); - let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); - let cell = CellRevision::new(data); - self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); + self.payload + .cell_by_field_id + .insert(field_id.to_owned(), insert_date_cell(timestamp, field_rev)); } } } pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) { match self.field_rev_map.get(&field_id.to_owned()) { - None => { - tracing::warn!("Invalid field_id: {}", field_id); - } + None => tracing::warn!("Can't find the select option field with id: {}", field_id), Some(field_rev) => { - let cell_data = SelectOptionCellChangeset::from_insert(&data).to_str(); - let data = apply_cell_data_changeset(cell_data, None, field_rev).unwrap(); - let cell = CellRevision::new(data); - self.payload.cell_by_field_id.insert(field_id.to_owned(), cell); + self.payload + .cell_by_field_id + .insert(field_id.to_owned(), insert_select_option_cell(data, field_rev)); } } } diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index ca1d92a54c..047c81ab02 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -114,20 +114,15 @@ pub fn make_default_board() -> BuildGridContext { row_builder.insert_select_option_cell(&multi_select_field_id, apple_option.id.clone()); row_builder.insert_select_option_cell(&multi_select_field_id, banana_option.id.clone()); // insert text - row_builder.insert_cell(&text_field_id, format!("Card {}", i)); + row_builder.insert_text_cell(&text_field_id, format!("Card {}", i)); // insert date row_builder.insert_date_cell(&date_field_id, timestamp); // number - row_builder.insert_cell(&number_field_id, format!("{}", i)); + row_builder.insert_number_cell(&number_field_id, i); // checkbox - let is_check = if i % 2 == 0 { - CHECK.to_string() - } else { - UNCHECK.to_string() - }; - row_builder.insert_cell(&checkbox_field_id, is_check); + row_builder.insert_checkbox_cell(&checkbox_field_id, i % 2 == 0); // url - row_builder.insert_cell(&url_field_id, "https://appflowy.io".to_string()); + row_builder.insert_url_cell(&url_field_id, "https://appflowy.io".to_string()); let row = row_builder.build(); grid_builder.add_row(row); diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index a733926228..848b2876fa 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -26,14 +26,14 @@ impl<'a> GridRowTestBuilder<'a> { pub fn insert_text_cell(&mut self, data: &str) -> String { let text_field = self.field_rev_with_type(&FieldType::RichText); - self.inner_builder.insert_cell(&text_field.id, data.to_string()); + self.inner_builder.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_rev_with_type(&FieldType::Number); - self.inner_builder.insert_cell(&number_field.id, data.to_string()); + self.inner_builder.insert_text_cell(&number_field.id, data.to_string()); number_field.id.clone() } @@ -44,20 +44,21 @@ impl<'a> GridRowTestBuilder<'a> { }) .unwrap(); let date_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.insert_cell(&date_field.id, value); + self.inner_builder.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_rev_with_type(&FieldType::Checkbox); - self.inner_builder.insert_cell(&checkbox_field.id, data.to_string()); + self.inner_builder + .insert_text_cell(&checkbox_field.id, data.to_string()); checkbox_field.id.clone() } pub fn insert_url_cell(&mut self, data: &str) -> String { let url_field = self.field_rev_with_type(&FieldType::URL); - self.inner_builder.insert_cell(&url_field.id, data.to_string()); + self.inner_builder.insert_text_cell(&url_field.id, data.to_string()); url_field.id.clone() } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index 46b278a1af..e47cf1287d 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -44,7 +44,7 @@ impl GridRevisionPad { .blocks .iter() .map(|block| { - let mut duplicated_block = (&*block.clone()).clone(); + let mut duplicated_block = (&**block).clone(); duplicated_block.block_id = gen_block_id(); duplicated_block }) From 24ca8da8c82bf4ad6e246ddd1c9dc76a9ac0db0e Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 14 Aug 2022 15:15:56 +0800 Subject: [PATCH 116/224] chore: add create card notification --- .../flowy-grid/src/dart_notification.rs | 1 + .../rust-lib/flowy-grid/src/event_handler.rs | 5 +- .../flowy-grid/src/services/block_manager.rs | 39 +++++------ .../flowy-grid/src/services/grid_editor.rs | 22 ++++-- .../group/group_generator/checkbox_group.rs | 4 +- .../group/group_generator/generator.rs | 17 ++--- .../group_generator/select_option_group.rs | 10 +-- .../src/services/group/group_service.rs | 68 +++++++++++++------ .../src/services/row/row_builder.rs | 6 +- frontend/rust-lib/flowy-grid/src/util.rs | 2 +- 10 files changed, 101 insertions(+), 73 deletions(-) diff --git a/frontend/rust-lib/flowy-grid/src/dart_notification.rs b/frontend/rust-lib/flowy-grid/src/dart_notification.rs index 202b12eb81..4108da1f11 100644 --- a/frontend/rust-lib/flowy-grid/src/dart_notification.rs +++ b/frontend/rust-lib/flowy-grid/src/dart_notification.rs @@ -11,6 +11,7 @@ pub enum GridNotification { DidUpdateRow = 30, DidUpdateCell = 40, DidUpdateField = 50, + DidUpdateBoard = 60, } impl std::default::Default for GridNotification { diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index fa23e1f4cf..2c119c55dc 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -232,10 +232,7 @@ pub(crate) async fn get_row_handler( ) -> DataResult { let params: RowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; - let row = editor - .get_row_rev(¶ms.row_id) - .await? - .and_then(|row_rev| Some(make_row_from_row_rev(row_rev))); + let row = editor.get_row_rev(¶ms.row_id).await?.map(make_row_from_row_rev); data_result(OptionalRowPB { row }) } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index 8da912534f..b2b6ce63fc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -161,31 +161,26 @@ impl GridBlockManager { Ok(changesets) } - pub(crate) async fn move_row(&self, row_id: &str, from: usize, to: usize) -> FlowyResult<()> { - let editor = self.get_editor_from_row_id(row_id).await?; - let _ = editor.move_row(row_id, from, to).await?; + pub(crate) async fn move_row(&self, row_rev: Arc, from: usize, to: usize) -> FlowyResult<()> { + let editor = self.get_editor_from_row_id(&row_rev.id).await?; + let _ = editor.move_row(&row_rev.id, from, to).await?; - match editor.get_row_revs(Some(vec![Cow::Borrowed(row_id)])).await?.pop() { - None => {} - Some(row_rev) => { - let delete_row_id = row_rev.id.clone(); - let insert_row = InsertedRowPB { - index: Some(to as i32), - row: make_row_from_row_rev(row_rev), - }; + let delete_row_id = row_rev.id.clone(); + let insert_row = InsertedRowPB { + index: Some(to as i32), + row: make_row_from_row_rev(row_rev), + }; - let notified_changeset = GridBlockChangesetPB { - block_id: editor.block_id.clone(), - inserted_rows: vec![insert_row], - deleted_rows: vec![delete_row_id], - ..Default::default() - }; + let notified_changeset = GridBlockChangesetPB { + block_id: editor.block_id.clone(), + inserted_rows: vec![insert_row], + deleted_rows: vec![delete_row_id], + ..Default::default() + }; - let _ = self - .notify_did_update_block(&editor.block_id, notified_changeset) - .await?; - } - } + let _ = self + .notify_did_update_block(&editor.block_id, notified_changeset) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 53c9a681bd..0bdf92ec96 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -326,6 +326,7 @@ impl GridRevisionEditor { pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let _ = self.block_manager.delete_row(row_id).await?; + self.group_service.read().await.did_delete_card(row_id.to_owned()).await; Ok(()) } @@ -517,10 +518,22 @@ impl GridRevisionEditor { } pub async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> { - let _ = self.block_manager.move_row(row_id, from as usize, to as usize).await?; + match self.block_manager.get_row_rev(row_id).await? { + None => tracing::warn!("Move row failed, can not find the row:{}", row_id), + Some(row_rev) => { + let _ = self + .block_manager + .move_row(row_rev.clone(), from as usize, to as usize) + .await?; + } + } Ok(()) } + pub async fn move_board_card(&self, group_id: &str, from: i32, to: i32) -> FlowyResult<()> { + self.group_service.write().await.move_card(group_id, from, to).await; + Ok(()) + } pub async fn delta_bytes(&self) -> Bytes { self.grid_pad.read().await.delta_bytes() } @@ -558,10 +571,12 @@ impl GridRevisionEditor { .group_service .write() .await - .create_board_card(&mut row_rev, group_id) + .update_board_card(&mut row_rev, group_id) .await; - self.create_row_pb(row_rev, None).await + let row_pb = self.create_row_pb(row_rev, None).await?; + self.group_service.read().await.did_create_card(group_id, &row_pb).await; + Ok(row_pb) } #[tracing::instrument(level = "trace", skip_all, err)] @@ -579,7 +594,6 @@ impl GridRevisionEditor { Ok(row_rev) } - #[tracing::instrument(level = "trace", skip_all, err)] async fn create_row_pb(&self, row_rev: RowRevision, start_row_id: Option) -> FlowyResult { let row_pb = RowPB::from(&row_rev); let block_id = row_rev.block_id.clone(); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs index acc8cf8715..85c135d0ee 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -1,4 +1,4 @@ -use crate::entities::{CheckboxGroupConfigurationPB, RowPB}; +use crate::entities::CheckboxGroupConfigurationPB; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; use std::sync::Arc; @@ -32,7 +32,7 @@ impl GroupActionHandler for CheckboxGroupController { self.handle_rows(row_revs, field_rev) } - fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + fn update_card(&self, _row_rev: &mut RowRevision, _field_rev: &FieldRevision, _group_id: &str) { todo!() } } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index 856c026a80..a351913546 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -1,13 +1,13 @@ use crate::entities::{GroupPB, RowPB}; use crate::services::cell::{decode_any_cell_data, CellBytesParser}; use bytes::Bytes; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ FieldRevision, GroupConfigurationRevision, RowRevision, TypeOptionDataDeserializer, }; -use futures::future::BoxFuture; + use indexmap::IndexMap; -use lib_infra::future::{BoxResultFuture, FutureResult}; + use std::marker::PhantomData; use std::sync::Arc; @@ -39,7 +39,7 @@ pub trait GroupActionHandler: Send + Sync { fn field_id(&self) -> &str; fn get_groups(&self) -> Vec; fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()>; - fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); + fn update_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); } pub trait GroupActionHandler2: Send + Sync { @@ -141,7 +141,7 @@ where for row in rows { if let Some(cell_rev) = row.cells.get(&self.field_id) { let mut records: Vec = vec![]; - let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), &field_rev); + let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev); let cell_data = cell_bytes.parser::

()?; for group in self.groups_map.values() { if self.can_group(&group.content, &cell_data) { @@ -168,13 +168,6 @@ where Ok(()) } - - pub fn group_rows(&mut self, rows: &[Arc]) -> FlowyResult<()> { - for row in rows { - // let _ = self.handle_row(row)?; - } - Ok(()) - } } struct GroupRecord { diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index a3a133b24c..50628875a8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -1,8 +1,8 @@ -use crate::entities::{RowPB, SelectOptionGroupConfigurationPB}; +use crate::entities::SelectOptionGroupConfigurationPB; use crate::services::cell::insert_select_option_cell; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; -use lib_infra::future::FutureResult; + use std::sync::Arc; use crate::services::field::{ @@ -40,7 +40,7 @@ impl GroupActionHandler for SingleSelectGroupController { self.handle_rows(row_revs, field_rev) } - fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + fn update_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { let group: Option<&Group> = self.groups_map.get(group_id); match group { None => {} @@ -105,7 +105,7 @@ impl GroupActionHandler for MultiSelectGroupController { self.handle_rows(row_revs, field_rev) } - fn create_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { + fn update_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str) { let group: Option<&Group> = self.groups_map.get(group_id); match group { None => tracing::warn!("Can not find the group: {}", group_id), diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index fa5f805b7f..3fd84d014c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,20 +1,21 @@ -use crate::services::block_manager::GridBlockManager; -use crate::services::grid_editor_task::GridServiceTaskScheduler; -use crate::services::group::{ - CheckboxGroupController, Group, GroupActionHandler, GroupCellContentProvider, MultiSelectGroupController, - SingleSelectGroupController, -}; - +use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ - CheckboxGroupConfigurationPB, CreateBoardCardParams, DateGroupConfigurationPB, FieldType, GroupPB, + BoardCardChangesetPB, CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, NumberGroupConfigurationPB, RowPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, }; +use crate::services::block_manager::GridBlockManager; +use crate::services::grid_editor_task::GridServiceTaskScheduler; +use crate::services::group::{ + CheckboxGroupController, GroupActionHandler, GroupCellContentProvider, MultiSelectGroupController, + SingleSelectGroupController, +}; + use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision}; use flowy_sync::client_grid::GridRevisionPad; -use futures::future::BoxFuture; + use std::sync::Arc; use tokio::sync::RwLock; @@ -63,21 +64,17 @@ impl GridGroupService { } #[tracing::instrument(level = "debug", skip(self, row_rev))] - pub(crate) async fn create_board_card(&self, row_rev: &mut RowRevision, group_id: &str) { + pub(crate) async fn update_board_card(&self, row_rev: &mut RowRevision, group_id: &str) { if let Some(group_action_handler) = self.group_action_handler.as_ref() { - match self - .grid_pad - .read() - .await - .get_field_rev(group_action_handler.read().await.field_id()) - { + let field_id = group_action_handler.read().await.field_id().to_owned(); + + match self.grid_pad.read().await.get_field_rev(&field_id) { None => tracing::warn!("Fail to create card because the field does not exist"), Some((_, field_rev)) => { - tracing::trace!("Create card"); group_action_handler .write() .await - .create_card(row_rev, field_rev, group_id); + .update_card(row_rev, field_rev, group_id); } } } @@ -97,6 +94,37 @@ impl GridGroupService { } } + pub async fn move_card(&self, _group_id: &str, _from: i32, _to: i32) { + // BoardCardChangesetPB { + // group_id: "".to_string(), + // inserted_cards: vec![], + // deleted_cards: vec![], + // updated_cards: vec![] + // } + // let row_pb = make_row_from_row_rev(row_rev); + todo!() + } + + pub async fn did_delete_card(&self, _row_id: String) { + // let changeset = BoardCardChangesetPB::delete(group_id.to_owned(), vec![row_id]); + // self.notify_did_update_board(changeset).await; + todo!() + } + + pub async fn did_create_card(&self, group_id: &str, row_pb: &RowPB) { + let changeset = BoardCardChangesetPB::insert(group_id.to_owned(), vec![row_pb.clone()]); + self.notify_did_update_board(changeset).await; + } + + pub async fn notify_did_update_board(&self, changeset: BoardCardChangesetPB) { + if self.group_action_handler.is_none() { + return; + } + send_dart_notification(&changeset.group_id, GridNotification::DidUpdateBoard) + .payload(changeset) + .send(); + } + #[tracing::instrument(level = "trace", skip_all, err)] async fn build_groups( &mut self, @@ -158,7 +186,7 @@ fn find_group_field(field_revs: &[Arc]) -> Option> {} fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision { - let field_type: FieldType = field_rev.field_type_rev.clone().into(); + let field_type: FieldType = field_rev.field_type_rev.into(); let bytes: Bytes = match field_type { FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(), FieldType::Number => NumberGroupConfigurationPB::default().try_into().unwrap(), @@ -171,7 +199,7 @@ fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationR GroupConfigurationRevision { id: gen_grid_group_id(), field_id: field_rev.id.clone(), - field_type_rev: field_rev.field_type_rev.clone(), + field_type_rev: field_rev.field_type_rev, content: Some(bytes.to_vec()), } } diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index 016bbea128..ee586b3851 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -1,8 +1,8 @@ use crate::services::cell::{ - apply_cell_data_changeset, insert_checkbox_cell, insert_date_cell, insert_number_cell, insert_select_option_cell, - insert_text_cell, insert_url_cell, + insert_checkbox_cell, insert_date_cell, insert_number_cell, insert_select_option_cell, insert_text_cell, + insert_url_cell, }; -use crate::services::field::{DateCellChangesetPB, SelectOptionCellChangeset}; + use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; use std::collections::HashMap; diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index 047c81ab02..90bf2f2a26 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -76,7 +76,7 @@ pub fn make_default_board() -> BuildGridContext { let multi_select_type_option = MultiSelectTypeOptionBuilder::default() .add_option(banana_option.clone()) .add_option(apple_option.clone()) - .add_option(pear_option.clone()); + .add_option(pear_option); let multi_select_field = FieldBuilder::new(multi_select_type_option) .name("Fruit") .visibility(true) From 445d5f6222437cbb6c911d3b0be1a9f24db06bb0 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 14 Aug 2022 16:05:14 +0800 Subject: [PATCH 117/224] chore: disable create board --- frontend/app_flowy/lib/plugins/board/board.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 36d181ae3e..2954a7cbf9 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { From 8da6ed9d28566cb4f714d7557920d953aca073ef Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 14 Aug 2022 21:09:18 +0800 Subject: [PATCH 118/224] chore: add grid view revision struct --- .../src/entities/setting_entities.rs | 32 ++-- .../src/services/filter/filter_cache.rs | 2 +- .../flowy-grid/src/services/grid_editor.rs | 5 +- .../src/services/group/group_service.rs | 3 +- .../src/services/setting/setting_builder.rs | 10 +- .../tests/grid/filter_test/script.rs | 9 +- .../flowy-grid/tests/grid/grid_editor.rs | 3 +- .../src/revision/filter_rev.rs | 9 -- .../src/revision/grid_block.rs | 61 ++++++++ .../src/revision/grid_group.rs | 10 -- .../src/revision/grid_rev.rs | 63 +------- .../src/revision/grid_setting_rev.rs | 137 ++++++++---------- .../src/revision/grid_view.rs | 20 +++ .../flowy-grid-data-model/src/revision/mod.rs | 8 +- .../src/client_grid/grid_revision_pad.rs | 50 +++---- shared-lib/flowy-sync/src/entities/grid.rs | 4 +- 16 files changed, 195 insertions(+), 231 deletions(-) delete mode 100644 shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs create mode 100644 shared-lib/flowy-grid-data-model/src/revision/grid_block.rs delete mode 100644 shared-lib/flowy-grid-data-model/src/revision/grid_group.rs create mode 100644 shared-lib/flowy-grid-data-model/src/revision/grid_view.rs diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index ae8037a383..e970249661 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -5,7 +5,7 @@ use crate::entities::{ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::GridLayoutRevision; +use flowy_grid_data_model::revision::LayoutRevision; use flowy_sync::entities::grid::GridSettingChangesetParams; use std::collections::HashMap; use std::convert::TryInto; @@ -19,7 +19,7 @@ pub struct GridSettingPB { pub layouts: Vec, #[pb(index = 2)] - pub current_layout_type: GridLayoutType, + pub current_layout_type: Layout, #[pb(index = 3)] pub filter_configuration_by_field_id: HashMap, @@ -34,13 +34,13 @@ pub struct GridSettingPB { #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct GridLayoutPB { #[pb(index = 1)] - ty: GridLayoutType, + ty: Layout, } impl GridLayoutPB { pub fn all() -> Vec { let mut layouts = vec![]; - for layout_ty in GridLayoutType::iter() { + for layout_ty in Layout::iter() { layouts.push(GridLayoutPB { ty: layout_ty }) } @@ -50,31 +50,31 @@ impl GridLayoutPB { #[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)] #[repr(u8)] -pub enum GridLayoutType { +pub enum Layout { Table = 0, Board = 1, } -impl std::default::Default for GridLayoutType { +impl std::default::Default for Layout { fn default() -> Self { - GridLayoutType::Table + Layout::Table } } -impl std::convert::From for GridLayoutType { - fn from(rev: GridLayoutRevision) -> Self { +impl std::convert::From for Layout { + fn from(rev: LayoutRevision) -> Self { match rev { - GridLayoutRevision::Table => GridLayoutType::Table, - GridLayoutRevision::Board => GridLayoutType::Board, + LayoutRevision::Table => Layout::Table, + LayoutRevision::Board => Layout::Board, } } } -impl std::convert::From for GridLayoutRevision { - fn from(layout: GridLayoutType) -> Self { +impl std::convert::From for LayoutRevision { + fn from(layout: Layout) -> Self { match layout { - GridLayoutType::Table => GridLayoutRevision::Table, - GridLayoutType::Board => GridLayoutRevision::Board, + Layout::Table => LayoutRevision::Table, + Layout::Board => LayoutRevision::Board, } } } @@ -85,7 +85,7 @@ pub struct GridSettingChangesetPayloadPB { pub grid_id: String, #[pb(index = 2)] - pub layout_type: GridLayoutType, + pub layout_type: Layout, #[pb(index = 3, one_of)] pub insert_filter: Option, diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs index 1d3ed77389..85a53c4d6f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs @@ -108,7 +108,7 @@ pub(crate) async fn refresh_filter_cache( grid_pad: &Arc>, ) { let grid_pad = grid_pad.read().await; - let filters_revs = grid_pad.get_filters(None, field_ids).unwrap_or_default(); + let filters_revs = grid_pad.get_filters(field_ids).unwrap_or_default(); for filter_rev in filters_revs { match grid_pad.get_field_rev(&filter_rev.field_id) { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 0bdf92ec96..3b7a4e854f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -447,10 +447,9 @@ impl GridRevisionEditor { Ok(grid_setting) } - pub async fn get_grid_filter(&self, layout_type: &GridLayoutType) -> FlowyResult> { + pub async fn get_grid_filter(&self) -> FlowyResult> { let read_guard = self.grid_pad.read().await; - let layout_rev = layout_type.clone().into(); - match read_guard.get_filters(Some(&layout_rev), None) { + match read_guard.get_filters(None) { Some(filter_revs) => Ok(filter_revs .iter() .map(|filter_rev| filter_rev.as_ref().into()) diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 3fd84d014c..d03261079a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -83,8 +83,7 @@ impl GridGroupService { pub(crate) async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { let grid_pad = self.grid_pad.read().await; let setting = grid_pad.get_setting_rev(); - let layout = &setting.layout; - let configurations = setting.get_groups(layout, &field_rev.id, &field_rev.field_type_rev); + let configurations = setting.get_groups(&field_rev.id, &field_rev.field_type_rev); match configurations { None => default_group_configuration(field_rev), Some(mut configurations) => { diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index 03ed8c760b..6eb481c74f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -1,8 +1,8 @@ use crate::entities::{ - GridLayoutPB, GridLayoutType, GridSettingPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, + GridLayoutPB, GridSettingPB, Layout, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RepeatedGridSortPB, }; -use flowy_grid_data_model::revision::{FieldRevision, GridSettingRevision}; +use flowy_grid_data_model::revision::{FieldRevision, SettingRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; use std::collections::HashMap; use std::sync::Arc; @@ -12,7 +12,7 @@ pub struct GridSettingChangesetBuilder { } impl GridSettingChangesetBuilder { - pub fn new(grid_id: &str, layout_type: &GridLayoutType) -> Self { + pub fn new(grid_id: &str, layout_type: &Layout) -> Self { let params = GridSettingChangesetParams { grid_id: grid_id.to_string(), layout_type: layout_type.clone().into(), @@ -41,8 +41,8 @@ impl GridSettingChangesetBuilder { } } -pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[Arc]) -> GridSettingPB { - let current_layout_type: GridLayoutType = grid_setting_rev.layout.clone().into(); +pub fn make_grid_setting(grid_setting_rev: &SettingRevision, field_revs: &[Arc]) -> GridSettingPB { + let current_layout_type: Layout = grid_setting_rev.layout.clone().into(); let filters_by_field_id = grid_setting_rev .get_all_filters(field_revs) .map(|filters_by_field_id| { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index 267cb570eb..03ba9fa29c 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] #![allow(unused_imports)] -use flowy_grid::entities::{CreateGridFilterPayloadPB, GridLayoutType, GridSettingPB}; +use flowy_grid::entities::{CreateGridFilterPayloadPB, Layout, GridSettingPB}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; @@ -55,19 +55,18 @@ impl GridFilterTest { } FilterScript::InsertGridTableFilter { payload } => { let params: CreateGridFilterParams = payload.try_into().unwrap(); - let layout_type = GridLayoutType::Table; + let layout_type = Layout::Table; let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) .insert_filter(params) .build(); let _ = self.editor.update_grid_setting(params).await.unwrap(); } FilterScript::AssertTableFilterCount { count } => { - let layout_type = GridLayoutType::Table; - let filters = self.editor.get_grid_filter(&layout_type).await.unwrap(); + let filters = self.editor.get_grid_filter().await.unwrap(); assert_eq!(count as usize, filters.len()); } FilterScript::DeleteGridTableFilter { filter_id, field_rev} => { - let layout_type = GridLayoutType::Table; + let layout_type = Layout::Table; let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) .delete_filter(DeleteFilterParams { field_id: field_rev.id, filter_id, field_type_rev: field_rev.field_type_rev }) .build(); diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index 2528ce2b32..a83c3121a5 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -76,8 +76,7 @@ impl GridEditorTest { } pub async fn grid_filters(&self) -> Vec { - let layout_type = GridLayoutType::Table; - self.editor.get_grid_filter(&layout_type).await.unwrap() + self.editor.get_grid_filter().await.unwrap() } pub fn get_field_rev(&self, field_type: FieldType) -> &Arc { diff --git a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs deleted file mode 100644 index 7079b52229..0000000000 --- a/shared-lib/flowy-grid-data-model/src/revision/filter_rev.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] -pub struct FilterConfigurationRevision { - pub id: String, - pub field_id: String, - pub condition: u8, - pub content: Option, -} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs new file mode 100644 index 0000000000..def044f439 --- /dev/null +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_block.rs @@ -0,0 +1,61 @@ +use indexmap::IndexMap; +use nanoid::nanoid; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; + +pub fn gen_row_id() -> String { + nanoid!(6) +} + +pub const DEFAULT_ROW_HEIGHT: i32 = 42; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct GridBlockRevision { + pub block_id: String, + pub rows: Vec>, +} + +pub type FieldId = String; +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct RowRevision { + pub id: String, + pub block_id: String, + /// cells contains key/value pairs. + /// key: field id, + /// value: CellMeta + #[serde(with = "indexmap::serde_seq")] + pub cells: IndexMap, + pub height: i32, + pub visibility: bool, +} + +impl RowRevision { + pub fn new(block_id: &str) -> Self { + Self { + id: gen_row_id(), + block_id: block_id.to_owned(), + cells: Default::default(), + height: DEFAULT_ROW_HEIGHT, + visibility: true, + } + } +} +#[derive(Debug, Clone, Default)] +pub struct RowMetaChangeset { + pub row_id: String, + pub height: Option, + pub visibility: Option, + pub cell_by_field_id: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct CellRevision { + pub data: String, +} + +impl CellRevision { + pub fn new(data: String) -> Self { + Self { data } + } +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs deleted file mode 100644 index dae56bd123..0000000000 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_group.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::revision::FieldTypeRevision; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] -pub struct GroupConfigurationRevision { - pub id: String, - pub field_id: String, - pub field_type_rev: FieldTypeRevision, - pub content: Option>, -} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index 184f5266ed..691dd5e185 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -1,13 +1,10 @@ -use crate::revision::GridSettingRevision; +use crate::revision::{GridBlockRevision, SettingRevision}; use bytes::Bytes; use indexmap::IndexMap; use nanoid::nanoid; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::sync::Arc; -pub const DEFAULT_ROW_HEIGHT: i32 = 42; - pub fn gen_grid_id() -> String { // nanoid calculator https://zelark.github.io/nano-id-cc/ nanoid!(10) @@ -17,10 +14,6 @@ pub fn gen_block_id() -> String { nanoid!(10) } -pub fn gen_row_id() -> String { - nanoid!(6) -} - pub fn gen_field_id() -> String { nanoid!(6) } @@ -32,7 +25,7 @@ pub struct GridRevision { pub blocks: Vec>, #[serde(default)] - pub setting: GridSettingRevision, + pub setting: SettingRevision, } impl GridRevision { @@ -41,7 +34,7 @@ impl GridRevision { grid_id: grid_id.to_owned(), fields: vec![], blocks: vec![], - setting: GridSettingRevision::default(), + setting: SettingRevision::default(), } } @@ -97,12 +90,6 @@ impl GridBlockMetaRevisionChangeset { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct GridBlockRevision { - pub block_id: String, - pub rows: Vec>, -} - #[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)] pub struct FieldRevision { pub id: String, @@ -201,50 +188,6 @@ pub trait TypeOptionDataDeserializer { fn from_protobuf_bytes(bytes: Bytes) -> Self; } -pub type FieldId = String; -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct RowRevision { - pub id: String, - pub block_id: String, - /// cells contains key/value pairs. - /// key: field id, - /// value: CellMeta - #[serde(with = "indexmap::serde_seq")] - pub cells: IndexMap, - pub height: i32, - pub visibility: bool, -} - -impl RowRevision { - pub fn new(block_id: &str) -> Self { - Self { - id: gen_row_id(), - block_id: block_id.to_owned(), - cells: Default::default(), - height: DEFAULT_ROW_HEIGHT, - visibility: true, - } - } -} -#[derive(Debug, Clone, Default)] -pub struct RowMetaChangeset { - pub row_id: String, - pub height: Option, - pub visibility: Option, - pub cell_by_field_id: HashMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct CellRevision { - pub data: String, -} - -impl CellRevision { - pub fn new(data: String) -> Self { - Self { data } - } -} - #[derive(Clone, Default, Deserialize, Serialize)] pub struct BuildGridContext { pub field_revs: Vec>, diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 6181ca8195..817974436f 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -1,5 +1,3 @@ -use crate::revision::filter_rev::FilterConfigurationRevision; -use crate::revision::grid_group::GroupConfigurationRevision; use crate::revision::{FieldRevision, FieldTypeRevision}; use indexmap::IndexMap; use nanoid::nanoid; @@ -21,26 +19,23 @@ pub fn gen_grid_sort_id() -> String { nanoid!(6) } -pub type FilterConfigurations = SettingConfiguration; -pub type FilterConfigurationRevisionMap = GridObjectRevisionMap; +pub type FilterConfiguration = Configuration; pub type FilterConfigurationsByFieldId = HashMap>>; // -pub type GroupConfigurations = SettingConfiguration; -pub type GroupConfigurationRevisionMap = GridObjectRevisionMap; +pub type GroupConfiguration = Configuration; pub type GroupConfigurationsByFieldId = HashMap>>; // -pub type SortConfigurations = SettingConfiguration; -pub type SortConfigurationRevisionMap = GridObjectRevisionMap; +pub type SortConfigurations = Configuration; pub type SortConfigurationsByFieldId = HashMap>>; #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] -pub struct GridSettingRevision { - pub layout: GridLayoutRevision, +pub struct SettingRevision { + pub layout: LayoutRevision, - pub filters: FilterConfigurations, + pub filters: FilterConfiguration, #[serde(default)] - pub groups: GroupConfigurations, + pub groups: GroupConfiguration, #[serde(skip)] pub sorts: SortConfigurations, @@ -48,88 +43,83 @@ pub struct GridSettingRevision { #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] #[repr(u8)] -pub enum GridLayoutRevision { +pub enum LayoutRevision { Table = 0, Board = 1, } -impl ToString for GridLayoutRevision { +impl ToString for LayoutRevision { fn to_string(&self) -> String { let layout_rev = self.clone() as u8; layout_rev.to_string() } } -impl std::default::Default for GridLayoutRevision { +impl std::default::Default for LayoutRevision { fn default() -> Self { - GridLayoutRevision::Table + LayoutRevision::Table } } -impl GridSettingRevision { +impl SettingRevision { pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { - self.groups.get_all_objects(&self.layout, field_revs) + self.groups.get_all_objects(field_revs) } pub fn get_groups( &self, - layout: &GridLayoutRevision, field_id: &str, field_type_rev: &FieldTypeRevision, ) -> Option>> { - self.groups.get_objects(layout, field_id, field_type_rev) + self.groups.get_objects(field_id, field_type_rev) } pub fn get_mut_groups( &mut self, - layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, ) -> Option<&mut Vec>> { - self.groups.get_mut_objects(layout, field_id, field_type) + self.groups.get_mut_objects(field_id, field_type) } pub fn insert_group( &mut self, - layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, group_rev: GroupConfigurationRevision, ) { - self.groups.remove_all(layout); - self.groups.insert_object(layout, field_id, field_type, group_rev); + // only one group can be set + self.groups.remove_all(); + self.groups.insert_object(field_id, field_type, group_rev); } pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { - self.filters.get_all_objects(&self.layout, field_revs) + self.filters.get_all_objects(field_revs) } pub fn get_filters( &self, - layout: &GridLayoutRevision, field_id: &str, field_type_rev: &FieldTypeRevision, ) -> Option>> { - self.filters.get_objects(layout, field_id, field_type_rev) + self.filters.get_objects(field_id, field_type_rev) } pub fn get_mut_filters( &mut self, - layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, ) -> Option<&mut Vec>> { - self.filters.get_mut_objects(layout, field_id, field_type) + self.filters.get_mut_objects(field_id, field_type) } pub fn insert_filter( &mut self, - layout: &GridLayoutRevision, field_id: &str, field_type: &FieldTypeRevision, filter_rev: FilterConfigurationRevision, ) { - self.filters.insert_object(layout, field_id, field_type, filter_rev); + self.filters.insert_object(field_id, field_type, filter_rev); } pub fn get_all_sort(&self) -> Option { @@ -145,59 +135,40 @@ pub struct SortConfigurationRevision { #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] #[serde(transparent)] -pub struct SettingConfiguration +pub struct Configuration where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { - /// Each layout contains multiple key/value. /// Key: field_id /// Value: this value contains key/value. /// Key: FieldType, /// Value: the corresponding objects. #[serde(with = "indexmap::serde_seq")] - inner: IndexMap>>, + inner: IndexMap>, } -impl SettingConfiguration +impl Configuration where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { - pub fn get_mut_objects( - &mut self, - layout: &GridLayoutRevision, - field_id: &str, - field_type: &FieldTypeRevision, - ) -> Option<&mut Vec>> { + pub fn get_mut_objects(&mut self, field_id: &str, field_type: &FieldTypeRevision) -> Option<&mut Vec>> { let value = self .inner - .get_mut(layout) - .and_then(|object_rev_map_by_field_id| object_rev_map_by_field_id.get_mut(field_id)) + .get_mut(field_id) .and_then(|object_rev_map| object_rev_map.get_mut(field_type)); if value.is_none() { tracing::warn!("Can't find the {:?} with", std::any::type_name::()); } value } - pub fn get_objects( - &self, - layout: &GridLayoutRevision, - field_id: &str, - field_type_rev: &FieldTypeRevision, - ) -> Option>> { + pub fn get_objects(&self, field_id: &str, field_type_rev: &FieldTypeRevision) -> Option>> { self.inner - .get(layout) - .and_then(|object_rev_map_by_field_id| object_rev_map_by_field_id.get(field_id)) + .get(field_id) .and_then(|object_rev_map| object_rev_map.get(field_type_rev)) .cloned() } - pub fn get_all_objects( - &self, - layout: &GridLayoutRevision, - field_revs: &[Arc], - ) -> Option>>> { - // Acquire the read lock. - let object_rev_map_by_field_id = self.inner.get(layout)?; + pub fn get_all_objects(&self, field_revs: &[Arc]) -> Option>>> { // Get the objects according to the FieldType, so we need iterate the field_revs. let objects_by_field_id = field_revs .iter() @@ -205,7 +176,7 @@ where let field_type = &field_rev.field_type_rev; let field_id = &field_rev.id; - let object_rev_map = object_rev_map_by_field_id.get(field_id)?; + let object_rev_map = self.inner.get(field_id)?; let objects: Vec> = object_rev_map.get(field_type)?.clone(); Some((field_rev.id.clone(), objects)) }) @@ -213,17 +184,11 @@ where Some(objects_by_field_id) } - pub fn insert_object( - &mut self, - layout: &GridLayoutRevision, - field_id: &str, - field_type: &FieldTypeRevision, - object: T, - ) { - let object_rev_map_by_field_id = self.inner.entry(layout.clone()).or_insert_with(IndexMap::new); - let object_rev_map = object_rev_map_by_field_id + pub fn insert_object(&mut self, field_id: &str, field_type: &FieldTypeRevision, object: T) { + let object_rev_map = self + .inner .entry(field_id.to_string()) - .or_insert_with(GridObjectRevisionMap::::new); + .or_insert_with(ObjectIndexMap::::new); object_rev_map .entry(field_type.to_owned()) @@ -231,16 +196,14 @@ where .push(Arc::new(object)) } - pub fn remove_all(&mut self, layout: &GridLayoutRevision) { - if let Some(object_rev_map_by_field_id) = self.inner.get_mut(layout) { - object_rev_map_by_field_id.clear() - } + pub fn remove_all(&mut self) { + self.inner.clear() } } #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] #[serde(transparent)] -pub struct GridObjectRevisionMap +pub struct ObjectIndexMap where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { @@ -248,16 +211,16 @@ where pub object_by_field_type: IndexMap>>, } -impl GridObjectRevisionMap +impl ObjectIndexMap where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { pub fn new() -> Self { - GridObjectRevisionMap::default() + ObjectIndexMap::default() } } -impl std::ops::Deref for GridObjectRevisionMap +impl std::ops::Deref for ObjectIndexMap where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { @@ -268,7 +231,7 @@ where } } -impl std::ops::DerefMut for GridObjectRevisionMap +impl std::ops::DerefMut for ObjectIndexMap where T: Debug + Clone + Default + Eq + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static, { @@ -276,3 +239,19 @@ where &mut self.object_by_field_type } } + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +pub struct GroupConfigurationRevision { + pub id: String, + pub field_id: String, + pub field_type_rev: FieldTypeRevision, + pub content: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)] +pub struct FilterConfigurationRevision { + pub id: String, + pub field_id: String, + pub condition: u8, + pub content: Option, +} diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs new file mode 100644 index 0000000000..7cfbbb4e65 --- /dev/null +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -0,0 +1,20 @@ +use crate::revision::SettingRevision; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct GridViewRevision { + pub view_id: String, + + pub grid_id: String, + + pub setting: SettingRevision, + // TODO: Save the rows' order. + // For the moment, we just use the order returned from the GridRevision + // #[serde(rename = "row")] + // pub row_orders: Vec, +} + +// #[derive(Debug, Clone, Default, Serialize, Deserialize)] +// pub struct RowOrderRevision { +// pub row_id: String, +// } diff --git a/shared-lib/flowy-grid-data-model/src/revision/mod.rs b/shared-lib/flowy-grid-data-model/src/revision/mod.rs index 67b58978b5..7ea98d78e3 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/mod.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/mod.rs @@ -1,9 +1,9 @@ -mod filter_rev; -mod grid_group; +mod grid_block; mod grid_rev; mod grid_setting_rev; +mod grid_view; -pub use filter_rev::*; -pub use grid_group::*; +pub use grid_block::*; pub use grid_rev::*; pub use grid_setting_rev::*; +pub use grid_view::*; diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index e47cf1287d..8e04871c09 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -7,8 +7,8 @@ use crate::util::{cal_diff, make_delta_from_revisions}; use bytes::Bytes; use flowy_grid_data_model::revision::{ gen_block_id, gen_grid_filter_id, gen_grid_group_id, gen_grid_id, FieldRevision, FieldTypeRevision, - FilterConfigurationRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, GridLayoutRevision, - GridRevision, GridSettingRevision, GroupConfigurationRevision, + FilterConfigurationRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, GridRevision, + GroupConfigurationRevision, SettingRevision, }; use lib_infra::util::move_vec_element; use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; @@ -341,18 +341,13 @@ impl GridRevisionPad { }) } - pub fn get_setting_rev(&self) -> &GridSettingRevision { + pub fn get_setting_rev(&self) -> &SettingRevision { &self.grid_rev.setting } /// If layout is None, then the default layout will be the read from GridSettingRevision - pub fn get_filters( - &self, - layout: Option<&GridLayoutRevision>, - field_ids: Option>, - ) -> Option>> { + pub fn get_filters(&self, field_ids: Option>) -> Option>> { let mut filter_revs = vec![]; - let layout_ty = layout.unwrap_or(&self.grid_rev.setting.layout); let field_revs = self.get_field_revs(None).ok()?; field_revs.iter().for_each(|field_rev| { @@ -365,8 +360,7 @@ impl GridRevisionPad { // Only return the filters for the current fields' type. let field_id = &field_rev.id; let field_type_rev = &field_rev.field_type_rev; - if let Some(mut t_filter_revs) = self.grid_rev.setting.get_filters(layout_ty, field_id, field_type_rev) - { + if let Some(mut t_filter_revs) = self.grid_rev.setting.get_filters(field_id, field_type_rev) { filter_revs.append(&mut t_filter_revs); } } @@ -381,40 +375,30 @@ impl GridRevisionPad { ) -> CollaborateResult> { self.modify_grid(|grid_rev| { let mut is_changed = None; - let layout_rev = changeset.layout_type; if let Some(params) = changeset.insert_filter { - grid_rev.setting.insert_filter( - &layout_rev, - ¶ms.field_id, - ¶ms.field_type_rev, - make_filter_revision(¶ms), - ); - + grid_rev + .setting + .insert_filter(¶ms.field_id, ¶ms.field_type_rev, make_filter_revision(¶ms)); is_changed = Some(()) } if let Some(params) = changeset.delete_filter { - if let Some(filters) = - grid_rev - .setting - .get_mut_filters(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) + if let Some(filters) = grid_rev + .setting + .get_mut_filters(¶ms.field_id, ¶ms.field_type_rev) { filters.retain(|filter| filter.id != params.filter_id); } } if let Some(params) = changeset.insert_group { - grid_rev.setting.insert_group( - &layout_rev, - ¶ms.field_id, - ¶ms.field_type_rev, - make_group_revision(¶ms), - ); + grid_rev + .setting + .insert_group(¶ms.field_id, ¶ms.field_type_rev, make_group_revision(¶ms)); is_changed = Some(()); } if let Some(params) = changeset.delete_group { - if let Some(groups) = - grid_rev - .setting - .get_mut_groups(&layout_rev, ¶ms.field_id, ¶ms.field_type_rev) + if let Some(groups) = grid_rev + .setting + .get_mut_groups(¶ms.field_id, ¶ms.field_type_rev) { groups.retain(|filter| filter.id != params.group_id); } diff --git a/shared-lib/flowy-sync/src/entities/grid.rs b/shared-lib/flowy-sync/src/entities/grid.rs index 23c8658839..3be3d98267 100644 --- a/shared-lib/flowy-sync/src/entities/grid.rs +++ b/shared-lib/flowy-sync/src/entities/grid.rs @@ -1,8 +1,8 @@ -use flowy_grid_data_model::revision::{FieldTypeRevision, GridLayoutRevision}; +use flowy_grid_data_model::revision::{FieldTypeRevision, LayoutRevision}; pub struct GridSettingChangesetParams { pub grid_id: String, - pub layout_type: GridLayoutRevision, + pub layout_type: LayoutRevision, pub insert_filter: Option, pub delete_filter: Option, pub insert_group: Option, From 15e1479caa34de501a48d5914f68ae76367bb7f4 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 14 Aug 2022 23:01:53 +0800 Subject: [PATCH 119/224] chore: add GridViewRevisionPad --- .../src/services/block_revision_editor.rs | 16 +- .../flowy-grid/src/services/grid_editor.rs | 8 +- .../src/services/grid_view_editor.rs | 48 +++++ .../rust-lib/flowy-grid/src/services/mod.rs | 1 + .../src/revision/grid_setting_rev.rs | 4 +- .../src/revision/grid_view.rs | 29 ++- ...k_revsion_pad.rs => block_revision_pad.rs} | 98 +++++----- .../src/client_grid/grid_revision_pad.rs | 40 ++-- shared-lib/flowy-sync/src/client_grid/mod.rs | 6 +- .../src/client_grid/view_revision_pad.rs | 173 ++++++++++++++++++ shared-lib/flowy-sync/src/util.rs | 6 +- shared-lib/lib-ot/src/core/delta/delta.rs | 2 +- 12 files changed, 344 insertions(+), 87 deletions(-) create mode 100644 frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs rename shared-lib/flowy-sync/src/client_grid/{grid_block_revsion_pad.rs => block_revision_pad.rs} (83%) create mode 100644 shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs diff --git a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs index 0d5f5d206e..a674098bfc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs @@ -3,7 +3,7 @@ use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision}; use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder}; -use flowy_sync::client_grid::{GridBlockMetaChange, GridBlockRevisionPad}; +use flowy_sync::client_grid::{GridBlockRevisionChangeset, GridBlockRevisionPad}; use flowy_sync::entities::revision::Revision; use flowy_sync::util::make_delta_from_revisions; use lib_infra::future::FutureResult; @@ -29,8 +29,8 @@ impl GridBlockRevisionEditor { let cloud = Arc::new(GridBlockRevisionCloudService { token: token.to_owned(), }); - let block_meta_pad = rev_manager.load::(Some(cloud)).await?; - let pad = Arc::new(RwLock::new(block_meta_pad)); + let block_revision_pad = rev_manager.load::(Some(cloud)).await?; + let pad = Arc::new(RwLock::new(block_revision_pad)); let rev_manager = Arc::new(rev_manager); let user_id = user_id.to_owned(); let block_id = block_id.to_owned(); @@ -145,7 +145,7 @@ impl GridBlockRevisionEditor { async fn modify(&self, f: F) -> FlowyResult<()> where - F: for<'a> FnOnce(&'a mut GridBlockRevisionPad) -> FlowyResult>, + F: for<'a> FnOnce(&'a mut GridBlockRevisionPad) -> FlowyResult>, { let mut write_guard = self.pad.write().await; match f(&mut *write_guard)? { @@ -157,8 +157,8 @@ impl GridBlockRevisionEditor { Ok(()) } - async fn apply_change(&self, change: GridBlockMetaChange) -> FlowyResult<()> { - let GridBlockMetaChange { delta, md5 } = change; + async fn apply_change(&self, change: GridBlockRevisionChangeset) -> FlowyResult<()> { + let GridBlockRevisionChangeset { delta, md5 } = change; let user_id = self.user_id.clone(); let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); let delta_data = delta.json_bytes(); @@ -187,8 +187,8 @@ impl RevisionCloudService for GridBlockRevisionCloudService { } } -struct GridBlockMetaPadBuilder(); -impl RevisionObjectBuilder for GridBlockMetaPadBuilder { +struct GridBlockRevisionPadBuilder(); +impl RevisionObjectBuilder for GridBlockRevisionPadBuilder { type Output = GridBlockRevisionPad; fn build_object(object_id: &str, revisions: Vec) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 3b7a4e854f..3f9e11295a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -16,7 +16,7 @@ use bytes::Bytes; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::*; use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder}; -use flowy_sync::client_grid::{GridChangeset, GridRevisionPad, JsonDeserializer}; +use flowy_sync::client_grid::{GridRevisionChangeset, GridRevisionPad, JsonDeserializer}; use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; use flowy_sync::entities::revision::Revision; use flowy_sync::errors::CollaborateResult; @@ -608,7 +608,7 @@ impl GridRevisionEditor { async fn modify(&self, f: F) -> FlowyResult<()> where - F: for<'a> FnOnce(&'a mut GridRevisionPad) -> FlowyResult>, + F: for<'a> FnOnce(&'a mut GridRevisionPad) -> FlowyResult>, { let mut write_guard = self.grid_pad.write().await; if let Some(changeset) = f(&mut *write_guard)? { @@ -617,8 +617,8 @@ impl GridRevisionEditor { Ok(()) } - async fn apply_change(&self, change: GridChangeset) -> FlowyResult<()> { - let GridChangeset { delta, md5 } = change; + async fn apply_change(&self, change: GridRevisionChangeset) -> FlowyResult<()> { + let GridRevisionChangeset { delta, md5 } = change; let user_id = self.user.user_id()?; let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); let delta_data = delta.json_bytes(); diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs new file mode 100644 index 0000000000..f287f7f89d --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -0,0 +1,48 @@ +use flowy_error::{FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::GridViewRevision; +use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder}; +use flowy_sync::client_grid::GridViewRevisionPad; +use flowy_sync::entities::revision::Revision; +use lib_infra::future::FutureResult; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct GridViewRevisionEditor { + pad: Arc>, + rev_manager: Arc, +} + +impl GridViewRevisionEditor { + pub async fn new(token: &str, mut rev_manager: RevisionManager) -> FlowyResult { + let cloud = Arc::new(GridViewRevisionCloudService { + token: token.to_owned(), + }); + let view_revision_pad = rev_manager.load::(Some(cloud)).await?; + let pad = Arc::new(RwLock::new(view_revision_pad)); + let rev_manager = Arc::new(rev_manager); + + Ok(Self { pad, rev_manager }) + } +} + +struct GridViewRevisionCloudService { + #[allow(dead_code)] + token: String, +} + +impl RevisionCloudService for GridViewRevisionCloudService { + #[tracing::instrument(level = "trace", skip(self))] + fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(vec![]) }) + } +} + +struct GridViewRevisionPadBuilder(); +impl RevisionObjectBuilder for GridViewRevisionPadBuilder { + type Output = GridViewRevisionPad; + + fn build_object(object_id: &str, revisions: Vec) -> FlowyResult { + let pad = GridViewRevisionPad::from_revisions(object_id, revisions)?; + Ok(pad) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/mod.rs b/frontend/rust-lib/flowy-grid/src/services/mod.rs index dc45575ab3..2683d1f06a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/mod.rs @@ -7,6 +7,7 @@ pub mod field; mod filter; pub mod grid_editor; mod grid_editor_task; +pub mod grid_view_editor; pub mod group; pub mod persistence; pub mod row; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs index 817974436f..c9d3b034d8 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_setting_rev.rs @@ -25,7 +25,7 @@ pub type FilterConfigurationsByFieldId = HashMap; pub type GroupConfigurationsByFieldId = HashMap>>; // -pub type SortConfigurations = Configuration; +pub type SortConfiguration = Configuration; pub type SortConfigurationsByFieldId = HashMap>>; #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] @@ -38,7 +38,7 @@ pub struct SettingRevision { pub groups: GroupConfiguration, #[serde(skip)] - pub sorts: SortConfigurations, + pub sorts: SortConfiguration, } #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize_repr, Deserialize_repr)] diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs index 7cfbbb4e65..7331797541 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -1,6 +1,11 @@ use crate::revision::SettingRevision; +use nanoid::nanoid; use serde::{Deserialize, Serialize}; +pub fn gen_grid_view_id() -> String { + nanoid!(6) +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct GridViewRevision { pub view_id: String, @@ -8,13 +13,23 @@ pub struct GridViewRevision { pub grid_id: String, pub setting: SettingRevision, - // TODO: Save the rows' order. + // For the moment, we just use the order returned from the GridRevision - // #[serde(rename = "row")] - // pub row_orders: Vec, + #[allow(dead_code)] + #[serde(skip, rename = "row")] + pub row_orders: Vec, } -// #[derive(Debug, Clone, Default, Serialize, Deserialize)] -// pub struct RowOrderRevision { -// pub row_id: String, -// } +impl GridViewRevision { + pub fn new(grid_id: String) -> Self { + let mut view_rev = GridViewRevision::default(); + view_rev.grid_id = grid_id; + view_rev.view_id = gen_grid_view_id(); + view_rev + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RowOrderRevision { + pub row_id: String, +} diff --git a/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs similarity index 83% rename from shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs rename to shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs index 51a331ecf7..e960ae42e0 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs @@ -1,6 +1,6 @@ use crate::entities::revision::{md5, RepeatedRevision, Revision}; use crate::errors::{CollaborateError, CollaborateResult}; -use crate::util::{cal_diff, make_delta_from_revisions}; +use crate::util::{cal_diff, make_delta_from_revisions, make_text_delta_from_revisions}; use flowy_grid_data_model::revision::{ gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision, }; @@ -9,27 +9,24 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -pub type GridBlockRevisionDelta = TextDelta; -pub type GridBlockRevisionDeltaBuilder = TextDeltaBuilder; - #[derive(Debug, Clone)] pub struct GridBlockRevisionPad { - block_revision: GridBlockRevision, - pub(crate) delta: GridBlockRevisionDelta, + block: GridBlockRevision, + delta: TextDelta, } impl std::ops::Deref for GridBlockRevisionPad { type Target = GridBlockRevision; fn deref(&self) -> &Self::Target { - &self.block_revision + &self.block } } impl GridBlockRevisionPad { pub async fn duplicate_data(&self, duplicated_block_id: &str) -> GridBlockRevision { let duplicated_rows = self - .block_revision + .block .rows .iter() .map(|row| { @@ -45,18 +42,18 @@ impl GridBlockRevisionPad { } } - pub fn from_delta(delta: GridBlockRevisionDelta) -> CollaborateResult { + pub fn from_delta(delta: TextDelta) -> CollaborateResult { let s = delta.content()?; - let block_revision: GridBlockRevision = serde_json::from_str(&s).map_err(|e| { - let msg = format!("Deserialize delta to block meta failed: {}", e); + let revision: GridBlockRevision = serde_json::from_str(&s).map_err(|e| { + let msg = format!("Deserialize delta to GridBlockRevision failed: {}", e); tracing::error!("{}", s); CollaborateError::internal().context(msg) })?; - Ok(Self { block_revision, delta }) + Ok(Self { block: revision, delta }) } pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { - let block_delta: GridBlockRevisionDelta = make_delta_from_revisions::(revisions)?; + let block_delta: TextDelta = make_text_delta_from_revisions(revisions)?; Self::from_delta(block_delta) } @@ -65,7 +62,7 @@ impl GridBlockRevisionPad { &mut self, row: RowRevision, start_row_id: Option, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.modify(|rows| { if let Some(start_row_id) = start_row_id { if !start_row_id.is_empty() { @@ -81,7 +78,10 @@ impl GridBlockRevisionPad { }) } - pub fn delete_rows(&mut self, row_ids: Vec>) -> CollaborateResult> { + pub fn delete_rows( + &mut self, + row_ids: Vec>, + ) -> CollaborateResult> { self.modify(|rows| { rows.retain(|row| !row_ids.contains(&Cow::Borrowed(&row.id))); Ok(Some(())) @@ -93,10 +93,10 @@ impl GridBlockRevisionPad { T: AsRef + ToOwned + ?Sized, { match row_ids { - None => Ok(self.block_revision.rows.clone()), + None => Ok(self.block.rows.clone()), Some(row_ids) => { let row_map = self - .block_revision + .block .rows .iter() .map(|row| (row.id.as_str(), row.clone())) @@ -136,18 +136,18 @@ impl GridBlockRevisionPad { } pub fn number_of_rows(&self) -> i32 { - self.block_revision.rows.len() as i32 + self.block.rows.len() as i32 } pub fn index_of_row(&self, row_id: &str) -> Option { - self.block_revision + self.block .rows .iter() .position(|row| row.id == row_id) .map(|index| index as i32) } - pub fn update_row(&mut self, changeset: RowMetaChangeset) -> CollaborateResult> { + pub fn update_row(&mut self, changeset: RowMetaChangeset) -> CollaborateResult> { let row_id = changeset.row_id.clone(); self.modify_row(&row_id, |row| { let mut is_changed = None; @@ -172,7 +172,12 @@ impl GridBlockRevisionPad { }) } - pub fn move_row(&mut self, row_id: &str, from: usize, to: usize) -> CollaborateResult> { + pub fn move_row( + &mut self, + row_id: &str, + from: usize, + to: usize, + ) -> CollaborateResult> { self.modify(|row_revs| { if let Some(position) = row_revs.iter().position(|row_rev| row_rev.id == row_id) { debug_assert_eq!(from, position); @@ -185,33 +190,36 @@ impl GridBlockRevisionPad { }) } - pub fn modify(&mut self, f: F) -> CollaborateResult> + pub fn modify(&mut self, f: F) -> CollaborateResult> where F: for<'a> FnOnce(&'a mut Vec>) -> CollaborateResult>, { let cloned_self = self.clone(); - match f(&mut self.block_revision.rows)? { + match f(&mut self.block.rows)? { None => Ok(None), Some(_) => { - let old = cloned_self.to_json()?; - let new = self.to_json()?; + let old = cloned_self.revision_json()?; + let new = self.revision_json()?; match cal_diff::(old, new) { None => Ok(None), Some(delta) => { - tracing::trace!("[GridBlockMeta] Composing delta {}", delta.json_str()); + tracing::trace!("[GridBlockRevision] Composing delta {}", delta.json_str()); // tracing::debug!( // "[GridBlockMeta] current delta: {}", // self.delta.to_str().unwrap_or_else(|_| "".to_string()) // ); self.delta = self.delta.compose(&delta)?; - Ok(Some(GridBlockMetaChange { delta, md5: self.md5() })) + Ok(Some(GridBlockRevisionChangeset { + delta, + md5: md5(&self.delta.json_bytes()), + })) } } } } } - fn modify_row(&mut self, row_id: &str, f: F) -> CollaborateResult> + fn modify_row(&mut self, row_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut RowRevision) -> CollaborateResult>, { @@ -225,27 +233,23 @@ impl GridBlockRevisionPad { }) } - pub fn to_json(&self) -> CollaborateResult { - serde_json::to_string(&self.block_revision) - .map_err(|e| CollaborateError::internal().context(format!("serial trash to json failed: {}", e))) + pub fn revision_json(&self) -> CollaborateResult { + serde_json::to_string(&self.block) + .map_err(|e| CollaborateError::internal().context(format!("serial block to json failed: {}", e))) } - pub fn md5(&self) -> String { - md5(&self.delta.json_bytes()) - } - - pub fn delta_str(&self) -> String { + pub fn json_str(&self) -> String { self.delta.json_str() } } -pub struct GridBlockMetaChange { - pub delta: GridBlockRevisionDelta, +pub struct GridBlockRevisionChangeset { + pub delta: TextDelta, /// md5: the md5 of the grid after applying the change. pub md5: String, } -pub fn make_grid_block_delta(block_rev: &GridBlockRevision) -> GridBlockRevisionDelta { +pub fn make_grid_block_delta(block_rev: &GridBlockRevision) -> TextDelta { let json = serde_json::to_string(&block_rev).unwrap(); TextDeltaBuilder::new().insert(&json).build() } @@ -265,14 +269,18 @@ impl std::default::Default for GridBlockRevisionPad { }; let delta = make_grid_block_delta(&block_revision); - GridBlockRevisionPad { block_revision, delta } + GridBlockRevisionPad { + block: block_revision, + delta, + } } } #[cfg(test)] mod tests { - use crate::client_grid::{GridBlockRevisionDelta, GridBlockRevisionPad}; + use crate::client_grid::GridBlockRevisionPad; use flowy_grid_data_model::revision::{RowMetaChangeset, RowRevision}; + use lib_ot::core::TextDelta; use std::borrow::Cow; #[test] @@ -369,7 +377,7 @@ mod tests { #[test] fn block_meta_delete_row() { let mut pad = test_pad(); - let pre_delta_str = pad.delta_str(); + let pre_delta_str = pad.json_str(); let row = RowRevision { id: "1".to_string(), block_id: pad.block_id.clone(), @@ -382,7 +390,7 @@ mod tests { let change = pad.delete_rows(vec![Cow::Borrowed(&row.id)]).unwrap().unwrap(); assert_eq!(change.delta.json_str(), r#"[{"retain":24},{"delete":66},{"retain":2}]"#); - assert_eq!(pad.delta_str(), pre_delta_str); + assert_eq!(pad.json_str(), pre_delta_str); } #[test] @@ -412,13 +420,13 @@ mod tests { ); assert_eq!( - pad.to_json().unwrap(), + pad.revision_json().unwrap(), r#"{"block_id":"1","rows":[{"id":"1","block_id":"1","cells":[],"height":100,"visibility":true}]}"# ); } fn test_pad() -> GridBlockRevisionPad { - let delta = GridBlockRevisionDelta::from_json(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap(); + let delta = TextDelta::from_json(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap(); GridBlockRevisionPad::from_delta(delta).unwrap() } } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index 8e04871c09..25a7de66a8 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -77,7 +77,7 @@ impl GridRevisionPad { &mut self, new_field_rev: FieldRevision, start_field_id: Option, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.modify_grid(|grid_meta| { // Check if the field exists or not if grid_meta @@ -102,7 +102,7 @@ impl GridRevisionPad { }) } - pub fn delete_field_rev(&mut self, field_id: &str) -> CollaborateResult> { + pub fn delete_field_rev(&mut self, field_id: &str) -> CollaborateResult> { self.modify_grid( |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { None => Ok(None), @@ -118,7 +118,7 @@ impl GridRevisionPad { &mut self, field_id: &str, duplicated_field_id: &str, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.modify_grid( |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { None => Ok(None), @@ -138,7 +138,7 @@ impl GridRevisionPad { field_id: &str, field_type: T, type_option_json_builder: B, - ) -> CollaborateResult> + ) -> CollaborateResult> where B: FnOnce(&FieldTypeRevision) -> String, T: Into, @@ -169,7 +169,7 @@ impl GridRevisionPad { &mut self, changeset: FieldChangesetParams, deserializer: T, - ) -> CollaborateResult> { + ) -> CollaborateResult> { let field_id = changeset.field_id.clone(); self.modify_field(&field_id, |field| { let mut is_changed = None; @@ -228,7 +228,10 @@ impl GridRevisionPad { .find(|(_, field)| field.id == field_id) } - pub fn replace_field_rev(&mut self, field_rev: Arc) -> CollaborateResult> { + pub fn replace_field_rev( + &mut self, + field_rev: Arc, + ) -> CollaborateResult> { self.modify_grid( |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_rev.id) { None => Ok(None), @@ -246,7 +249,7 @@ impl GridRevisionPad { field_id: &str, from_index: usize, to_index: usize, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.modify_grid(|grid_meta| { match move_vec_element( &mut grid_meta.fields, @@ -292,7 +295,10 @@ impl GridRevisionPad { } } - pub fn create_block_meta_rev(&mut self, block: GridBlockMetaRevision) -> CollaborateResult> { + pub fn create_block_meta_rev( + &mut self, + block: GridBlockMetaRevision, + ) -> CollaborateResult> { self.modify_grid(|grid_meta| { if grid_meta.blocks.iter().any(|b| b.block_id == block.block_id) { tracing::warn!("Duplicate grid block"); @@ -322,7 +328,7 @@ impl GridRevisionPad { pub fn update_block_rev( &mut self, changeset: GridBlockMetaRevisionChangeset, - ) -> CollaborateResult> { + ) -> CollaborateResult> { let block_id = changeset.block_id.clone(); self.modify_block(&block_id, |block| { let mut is_changed = None; @@ -372,7 +378,7 @@ impl GridRevisionPad { pub fn update_grid_setting_rev( &mut self, changeset: GridSettingChangesetParams, - ) -> CollaborateResult> { + ) -> CollaborateResult> { self.modify_grid(|grid_rev| { let mut is_changed = None; if let Some(params) = changeset.insert_filter { @@ -446,7 +452,7 @@ impl GridRevisionPad { &self.grid_rev.fields } - fn modify_grid(&mut self, f: F) -> CollaborateResult> + fn modify_grid(&mut self, f: F) -> CollaborateResult> where F: FnOnce(&mut GridRevision) -> CollaborateResult>, { @@ -460,14 +466,14 @@ impl GridRevisionPad { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; - Ok(Some(GridChangeset { delta, md5: self.md5() })) + Ok(Some(GridRevisionChangeset { delta, md5: self.md5() })) } } } } } - fn modify_block(&mut self, block_id: &str, f: F) -> CollaborateResult> + fn modify_block(&mut self, block_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut GridBlockMetaRevision) -> CollaborateResult>, { @@ -485,7 +491,7 @@ impl GridRevisionPad { ) } - fn modify_field(&mut self, field_id: &str, f: F) -> CollaborateResult> + fn modify_field(&mut self, field_id: &str, f: F) -> CollaborateResult> where F: FnOnce(&mut FieldRevision) -> CollaborateResult>, { @@ -508,13 +514,13 @@ impl GridRevisionPad { } } -pub fn make_grid_rev_json_str(grid: &GridRevision) -> CollaborateResult { - let json = serde_json::to_string(grid) +pub fn make_grid_rev_json_str(grid_revision: &GridRevision) -> CollaborateResult { + let json = serde_json::to_string(grid_revision) .map_err(|err| internal_error(format!("Serialize grid to json str failed. {:?}", err)))?; Ok(json) } -pub struct GridChangeset { +pub struct GridRevisionChangeset { pub delta: GridRevisionDelta, /// md5: the md5 of the grid after applying the change. pub md5: String, diff --git a/shared-lib/flowy-sync/src/client_grid/mod.rs b/shared-lib/flowy-sync/src/client_grid/mod.rs index e76ae0cefe..4a9a0374f5 100644 --- a/shared-lib/flowy-sync/src/client_grid/mod.rs +++ b/shared-lib/flowy-sync/src/client_grid/mod.rs @@ -1,7 +1,9 @@ -mod grid_block_revsion_pad; +mod block_revision_pad; mod grid_builder; mod grid_revision_pad; +mod view_revision_pad; -pub use grid_block_revsion_pad::*; +pub use block_revision_pad::*; pub use grid_builder::*; pub use grid_revision_pad::*; +pub use view_revision_pad::*; diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs new file mode 100644 index 0000000000..25b539faf6 --- /dev/null +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -0,0 +1,173 @@ +use crate::entities::revision::{md5, Revision}; +use crate::errors::{internal_error, CollaborateError, CollaborateResult}; +use crate::util::{cal_diff, make_delta_from_revisions, make_text_delta_from_revisions}; +use flowy_grid_data_model::revision::{ + FieldRevision, FieldTypeRevision, FilterConfigurationRevision, FilterConfigurationsByFieldId, GridViewRevision, + GroupConfigurationRevision, GroupConfigurationsByFieldId, SortConfigurationsByFieldId, +}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct GridViewRevisionPad { + view: Arc, + delta: TextDelta, +} + +impl std::ops::Deref for GridViewRevisionPad { + type Target = GridViewRevision; + + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl GridViewRevisionPad { + pub fn new(grid_id: String) -> Self { + let view = Arc::new(GridViewRevision::new(grid_id)); + let json = serde_json::to_string(&view).unwrap(); + let delta = TextDeltaBuilder::new().insert(&json).build(); + Self { view, delta } + } + + pub fn from_delta(delta: TextDelta) -> CollaborateResult { + let s = delta.content()?; + let view: GridViewRevision = serde_json::from_str(&s).map_err(|e| { + let msg = format!("Deserialize delta to GridViewRevision failed: {}", e); + tracing::error!("{}", s); + CollaborateError::internal().context(msg) + })?; + Ok(Self { + view: Arc::new(view), + delta, + }) + } + + pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { + let delta: TextDelta = make_text_delta_from_revisions(revisions)?; + Self::from_delta(delta) + } + + pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { + self.setting.groups.get_all_objects(field_revs) + } + + pub fn get_groups( + &self, + field_id: &str, + field_type_rev: &FieldTypeRevision, + ) -> Option>> { + self.setting.groups.get_objects(field_id, field_type_rev) + } + + pub fn insert_group( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + group_rev: GroupConfigurationRevision, + ) -> CollaborateResult> { + self.modify(|view| { + // only one group can be set + view.setting.groups.remove_all(); + view.setting.groups.insert_object(field_id, field_type, group_rev); + Ok(Some(())) + }) + } + + pub fn delete_group( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + group_id: &str, + ) -> CollaborateResult> { + self.modify(|view| { + if let Some(groups) = view.setting.groups.get_mut_objects(field_id, field_type) { + groups.retain(|group| group.id != group_id); + Ok(Some(())) + } else { + Ok(None) + } + }) + } + + pub fn get_all_filters(&self, field_revs: &[Arc]) -> Option { + self.setting.filters.get_all_objects(field_revs) + } + + pub fn get_filters( + &self, + field_id: &str, + field_type_rev: &FieldTypeRevision, + ) -> Option>> { + self.setting.filters.get_objects(field_id, field_type_rev) + } + + pub fn insert_filter( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + filter_rev: FilterConfigurationRevision, + ) -> CollaborateResult> { + self.modify(|view| { + view.setting.filters.insert_object(field_id, field_type, filter_rev); + Ok(Some(())) + }) + } + + pub fn delete_filter( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + filter_id: &str, + ) -> CollaborateResult> { + self.modify(|view| { + if let Some(filters) = view.setting.filters.get_mut_objects(field_id, field_type) { + filters.retain(|filter| filter.id != filter_id); + Ok(Some(())) + } else { + Ok(None) + } + }) + } + + pub fn get_all_sort(&self) -> Option { + None + } + + pub fn json_str(&self) -> CollaborateResult { + make_grid_view_rev_json_str(&self.view) + } + + fn modify(&mut self, f: F) -> CollaborateResult> + where + F: FnOnce(&mut GridViewRevision) -> CollaborateResult>, + { + let cloned_view = self.view.clone(); + match f(Arc::make_mut(&mut self.view))? { + None => Ok(None), + Some(_) => { + let old = make_grid_view_rev_json_str(&cloned_view)?; + let new = self.json_str()?; + match cal_diff::(old, new) { + None => Ok(None), + Some(delta) => { + self.delta = self.delta.compose(&delta)?; + let md5 = md5(&self.delta.json_bytes()); + Ok(Some(GridViewRevisionChangeset { delta, md5 })) + } + } + } + } + } +} + +pub struct GridViewRevisionChangeset { + pub delta: TextDelta, + pub md5: String, +} + +pub fn make_grid_view_rev_json_str(grid_revision: &GridViewRevision) -> CollaborateResult { + let json = serde_json::to_string(grid_revision) + .map_err(|err| internal_error(format!("Serialize grid view to json str failed. {:?}", err)))?; + Ok(json) +} diff --git a/shared-lib/flowy-sync/src/util.rs b/shared-lib/flowy-sync/src/util.rs index 7dd5c4af5c..d968d74796 100644 --- a/shared-lib/flowy-sync/src/util.rs +++ b/shared-lib/flowy-sync/src/util.rs @@ -7,7 +7,7 @@ use crate::{ errors::{CollaborateError, CollaborateResult}, }; use dissimilar::Chunk; -use lib_ot::core::{DeltaBuilder, OTString}; +use lib_ot::core::{DeltaBuilder, OTString, PhantomAttributes, TextDelta}; use lib_ot::{ core::{Attributes, Delta, OperationTransform, NEW_LINE, WHITESPACE}, rich_text::RichTextDelta, @@ -81,6 +81,10 @@ where Ok(delta) } +pub fn make_text_delta_from_revisions(revisions: Vec) -> CollaborateResult { + make_delta_from_revisions::(revisions) +} + pub fn make_delta_from_revision_pb(revisions: Vec) -> CollaborateResult> where T: Attributes + DeserializeOwned, diff --git a/shared-lib/lib-ot/src/core/delta/delta.rs b/shared-lib/lib-ot/src/core/delta/delta.rs index b422205a90..a95fd7c236 100644 --- a/shared-lib/lib-ot/src/core/delta/delta.rs +++ b/shared-lib/lib-ot/src/core/delta/delta.rs @@ -604,7 +604,7 @@ where serde_json::to_string(self).unwrap_or_else(|_| "".to_owned()) } - /// Get the content the [Delta] represents. + /// Get the content that the [Delta] represents. pub fn content(&self) -> Result { self.apply("") } From 32e20a4dc7f2a5b616f4890719e83f9a12a80458 Mon Sep 17 00:00:00 2001 From: appflowy Date: Sun, 14 Aug 2022 23:11:30 +0800 Subject: [PATCH 120/224] chore: rename some struct --- .../flowy-grid/src/services/grid_editor.rs | 4 ++++ .../flowy-grid/src/services/grid_view_editor.rs | 6 +++++- .../disk/{text_rev_impl.rs => document_impl.rs} | 1 - .../src/cache/disk/folder_rev_impl.rs | 1 - ...d_block_meta_rev_impl.rs => grid_block_impl.rs} | 0 .../cache/disk/{grid_rev_impl.rs => grid_impl.rs} | 0 .../rust-lib/flowy-revision/src/cache/disk/mod.rs | 14 ++++++-------- .../src/revision/grid_view.rs | 10 ++++++---- .../src/client_grid/block_revision_pad.rs | 2 +- .../src/client_grid/view_revision_pad.rs | 2 +- 10 files changed, 23 insertions(+), 17 deletions(-) rename frontend/rust-lib/flowy-revision/src/cache/disk/{text_rev_impl.rs => document_impl.rs} (99%) delete mode 100644 frontend/rust-lib/flowy-revision/src/cache/disk/folder_rev_impl.rs rename frontend/rust-lib/flowy-revision/src/cache/disk/{grid_block_meta_rev_impl.rs => grid_block_impl.rs} (100%) rename frontend/rust-lib/flowy-revision/src/cache/disk/{grid_rev_impl.rs => grid_impl.rs} (100%) diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 3f9e11295a..ac5fe92c36 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -6,6 +6,7 @@ use crate::services::block_manager::GridBlockManager; use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, CellBytes}; use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder}; use crate::services::filter::{GridFilterChangeset, GridFilterService}; + use crate::services::group::GridGroupService; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{ @@ -31,8 +32,10 @@ pub struct GridRevisionEditor { pub(crate) grid_id: String, user: Arc, grid_pad: Arc>, + // view_editor: Arc, rev_manager: Arc, block_manager: Arc, + #[allow(dead_code)] pub(crate) filter_service: Arc, @@ -59,6 +62,7 @@ impl GridRevisionEditor { let grid_pad = rev_manager.load::(Some(cloud)).await?; let rev_manager = Arc::new(rev_manager); let grid_pad = Arc::new(RwLock::new(grid_pad)); + let block_meta_revs = grid_pad.read().await.get_block_meta_revs(); let block_manager = Arc::new(GridBlockManager::new(grid_id, &user, block_meta_revs, persistence).await?); let filter_service = diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index f287f7f89d..e37ca9896a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,5 +1,5 @@ use flowy_error::{FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::GridViewRevision; + use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder}; use flowy_sync::client_grid::GridViewRevisionPad; use flowy_sync::entities::revision::Revision; @@ -7,12 +7,16 @@ use lib_infra::future::FutureResult; use std::sync::Arc; use tokio::sync::RwLock; +#[allow(dead_code)] pub struct GridViewRevisionEditor { + #[allow(dead_code)] pad: Arc>, + #[allow(dead_code)] rev_manager: Arc, } impl GridViewRevisionEditor { + #[allow(dead_code)] pub async fn new(token: &str, mut rev_manager: RevisionManager) -> FlowyResult { let cloud = Arc::new(GridViewRevisionCloudService { token: token.to_owned(), diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/text_rev_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs similarity index 99% rename from frontend/rust-lib/flowy-revision/src/cache/disk/text_rev_impl.rs rename to frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs index 165864d5db..92525be9e0 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/text_rev_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs @@ -1,6 +1,5 @@ use crate::cache::disk::RevisionDiskCache; use crate::disk::{RevisionChangeset, RevisionRecord, RevisionState}; - use bytes::Bytes; use diesel::{sql_types::Integer, update, SqliteConnection}; use flowy_database::{ diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/folder_rev_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/folder_rev_impl.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/folder_rev_impl.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_meta_rev_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_impl.rs similarity index 100% rename from frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_meta_rev_impl.rs rename to frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_impl.rs diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_rev_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_impl.rs similarity index 100% rename from frontend/rust-lib/flowy-revision/src/cache/disk/grid_rev_impl.rs rename to frontend/rust-lib/flowy-revision/src/cache/disk/grid_impl.rs diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs index 991d8f9b9f..14614523ce 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs @@ -1,12 +1,10 @@ -mod folder_rev_impl; -mod grid_block_meta_rev_impl; -mod grid_rev_impl; -mod text_rev_impl; +mod document_impl; +mod grid_block_impl; +mod grid_impl; -pub use folder_rev_impl::*; -pub use grid_block_meta_rev_impl::*; -pub use grid_rev_impl::*; -pub use text_rev_impl::*; +pub use document_impl::*; +pub use grid_block_impl::*; +pub use grid_impl::*; use flowy_error::FlowyResult; use flowy_sync::entities::revision::{RevId, Revision, RevisionRange}; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs index 7331797541..2b8861ba49 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -22,10 +22,12 @@ pub struct GridViewRevision { impl GridViewRevision { pub fn new(grid_id: String) -> Self { - let mut view_rev = GridViewRevision::default(); - view_rev.grid_id = grid_id; - view_rev.view_id = gen_grid_view_id(); - view_rev + GridViewRevision { + view_id: gen_grid_view_id(), + grid_id, + setting: Default::default(), + row_orders: vec![], + } } } diff --git a/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs index e960ae42e0..4215da7572 100644 --- a/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs @@ -1,6 +1,6 @@ use crate::entities::revision::{md5, RepeatedRevision, Revision}; use crate::errors::{CollaborateError, CollaborateResult}; -use crate::util::{cal_diff, make_delta_from_revisions, make_text_delta_from_revisions}; +use crate::util::{cal_diff, make_text_delta_from_revisions}; use flowy_grid_data_model::revision::{ gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision, }; diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 25b539faf6..a3906a4aaf 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -1,6 +1,6 @@ use crate::entities::revision::{md5, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; -use crate::util::{cal_diff, make_delta_from_revisions, make_text_delta_from_revisions}; +use crate::util::{cal_diff, make_text_delta_from_revisions}; use flowy_grid_data_model::revision::{ FieldRevision, FieldTypeRevision, FilterConfigurationRevision, FilterConfigurationsByFieldId, GridViewRevision, GroupConfigurationRevision, GroupConfigurationsByFieldId, SortConfigurationsByFieldId, From b15439fb70a9dc3abcf2488ca746aa9f38bd38f1 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 15 Aug 2022 10:08:05 +0800 Subject: [PATCH 121/224] chore: add grid_view_table --- .../migrations/2022-08-15-020544_grid-view/down.sql | 2 ++ .../migrations/2022-08-15-020544_grid-view/up.sql | 11 +++++++++++ frontend/rust-lib/flowy-database/src/schema.rs | 12 ++++++++++++ .../rust-lib/flowy-grid/src/services/grid_editor.rs | 3 ++- 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql create mode 100644 frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql new file mode 100644 index 0000000000..c2b23b0180 --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE grid_view_table; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql new file mode 100644 index 0000000000..d6e526aba4 --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here + + +CREATE TABLE grid_view_table ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + object_id TEXT NOT NULL DEFAULT '', + base_rev_id BIGINT NOT NULL DEFAULT 0, + rev_id BIGINT NOT NULL DEFAULT 0, + data BLOB NOT NULL DEFAULT (x''), + state INTEGER NOT NULL DEFAULT 0 +); diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index eda9cd888b..ae616622b7 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -42,6 +42,17 @@ table! { } } +table! { + grid_view_table (id) { + id -> Integer, + object_id -> Text, + base_rev_id -> BigInt, + rev_id -> BigInt, + data -> Binary, + state -> Integer, + } +} + table! { kv_table (key) { key -> Text, @@ -125,6 +136,7 @@ allow_tables_to_appear_in_same_query!( grid_block_index_table, grid_meta_rev_table, grid_rev_table, + grid_view_table, kv_table, rev_snapshot, rev_table, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index ac5fe92c36..812ce561c1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -7,6 +7,7 @@ use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, Cel use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder}; use crate::services::filter::{GridFilterChangeset, GridFilterService}; +use crate::services::grid_view_editor::GridViewRevisionEditor; use crate::services::group::GridGroupService; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{ @@ -32,7 +33,7 @@ pub struct GridRevisionEditor { pub(crate) grid_id: String, user: Arc, grid_pad: Arc>, - // view_editor: Arc, + view_editor: Arc, rev_manager: Arc, block_manager: Arc, From 080c65562749536ee376b9601b70244ad23c8071 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 15 Aug 2022 10:09:04 +0800 Subject: [PATCH 122/224] chore: update down.sql of ext_data --- .../migrations/2022-06-11-090029_view-add-col/down.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-database/migrations/2022-06-11-090029_view-add-col/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-06-11-090029_view-add-col/down.sql index 291a97c5ce..df82040abc 100644 --- a/frontend/rust-lib/flowy-database/migrations/2022-06-11-090029_view-add-col/down.sql +++ b/frontend/rust-lib/flowy-database/migrations/2022-06-11-090029_view-add-col/down.sql @@ -1 +1,2 @@ --- This file should undo anything in `up.sql` \ No newline at end of file +-- This file should undo anything in `up.sql` +ALTER TABLE view_table DROP COLUMN ext_data; \ No newline at end of file From 0da986553cc71612ea98c353ed5165eab8d45aa0 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 15 Aug 2022 10:09:59 +0800 Subject: [PATCH 123/224] chore: update down.sql of user_tablea --- .../migrations/2021-07-14-022241_user-add-col/down.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/rust-lib/flowy-database/migrations/2021-07-14-022241_user-add-col/down.sql b/frontend/rust-lib/flowy-database/migrations/2021-07-14-022241_user-add-col/down.sql index f1119aa25a..b125f69360 100644 --- a/frontend/rust-lib/flowy-database/migrations/2021-07-14-022241_user-add-col/down.sql +++ b/frontend/rust-lib/flowy-database/migrations/2021-07-14-022241_user-add-col/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -DROP TABLE user_table; \ No newline at end of file +ALTER TABLE user_table DROP COLUMN workspace; \ No newline at end of file From a6bba5a0f9b19445b43e0bcd0da229811b3ccdd2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 13 Aug 2022 00:18:59 +0800 Subject: [PATCH 124/224] feat: implement editor test infra --- .../flowy_editor/lib/flowy_editor.dart | 1 + .../flowy_editor/lib/src/document/node.dart | 21 ++++ .../lib/src/document/state_tree.dart | 13 ++- .../flowy_editor/lib/src/editor_state.dart | 6 + .../lib/src/service/keyboard_service.dart | 24 ++-- ...thout_shift_in_text_node_handler_test.dart | 69 +++++++++++ .../flowy_editor/test/editor/widget_test.dart | 39 +++++++ .../flowy_editor/test/infra/test_editor.dart | 107 ++++++++++++++++++ .../test/infra/test_raw_key_event.dart | 52 +++++++++ .../test/{ => legacy}/delta_test.dart | 0 .../test/{ => legacy}/flowy_editor_test.dart | 0 .../test/{ => legacy}/operation_test.dart | 0 12 files changed, 322 insertions(+), 10 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart rename frontend/app_flowy/packages/flowy_editor/test/{ => legacy}/delta_test.dart (100%) rename frontend/app_flowy/packages/flowy_editor/test/{ => legacy}/flowy_editor_test.dart (100%) rename frontend/app_flowy/packages/flowy_editor/test/{ => legacy}/operation_test.dart (100%) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 418b4d7ce0..f134d309b0 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -6,6 +6,7 @@ export 'src/document/position.dart'; export 'src/document/selection.dart'; export 'src/document/state_tree.dart'; export 'src/document/text_delta.dart'; +export 'src/document/attributes.dart'; export 'src/editor_state.dart'; export 'src/operation/operation.dart'; export 'src/operation/transaction.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart index 6bfc877524..5bf63e63a3 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/node.dart @@ -111,6 +111,27 @@ class Node extends ChangeNotifier with LinkedListEntry { return childAtIndex(path.first)?.childAtPath(path.sublist(1)); } + void insert(Node entry, {int? index}) { + index ??= children.length; + + if (children.isEmpty) { + entry.parent = this; + children.add(entry); + notifyListeners(); + return; + } + + final length = children.length; + + if (index >= length) { + children.last.insertAfter(entry); + } else if (index <= 0) { + children.first.insertBefore(entry); + } else { + childAtIndex(index)?.insertBefore(entry); + } + } + @override void insertAfter(Node entry) { entry.parent = parent; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart index b356880310..f1437fbaef 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/document/path.dart'; import 'package:flowy_editor/src/document/text_delta.dart'; @@ -27,9 +29,18 @@ class StateTree { return false; } Node? insertedNode = root.childAtPath( - path.sublist(0, path.length - 1) + [path.last - 1], + path.sublist(0, path.length - 1) + [max(0, path.last - 1)], ); if (insertedNode == null) { + final insertedNode = root.childAtPath( + path.sublist(0, path.length - 1), + ); + if (insertedNode != null) { + for (final node in nodes) { + insertedNode.insert(node); + } + return true; + } return false; } for (var i = 0; i < nodes.length; i++) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 6b7210cd7a..4452e92ed2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -51,6 +51,9 @@ class EditorState { final UndoManager undoManager = UndoManager(); Selection? _cursorSelection; + /// TODO: only for testing. + bool disableSealTimer = false; + Selection? get cursorSelection { return _cursorSelection; } @@ -106,6 +109,9 @@ class EditorState { } _debouncedSealHistoryItem() { + if (disableSealTimer) { + return; + } _debouncedSealHistoryItemTimer?.cancel(); _debouncedSealHistoryItemTimer = Timer(const Duration(milliseconds: 1000), () { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart index ef8165049b..b7573e4ed6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; abstract class FlowyKeyboardService { + KeyEventResult onKey(RawKeyEvent event); void enable(); void disable(); } @@ -65,15 +66,8 @@ class _FlowyKeyboardState extends State _focusNode.unfocus(); } - void _onFocusChange(bool value) { - debugPrint('[KeyBoard Service] focus change $value'); - } - - KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { - if (!isFocus) { - return KeyEventResult.ignored; - } - + @override + KeyEventResult onKey(RawKeyEvent event) { debugPrint('on keyboard event $event'); if (event is! RawKeyDownEvent) { @@ -97,4 +91,16 @@ class _FlowyKeyboardState extends State return KeyEventResult.ignored; } + + void _onFocusChange(bool value) { + debugPrint('[KeyBoard Service] focus change $value'); + } + + KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { + if (!isFocus) { + return KeyEventResult.ignored; + } + + return onKey(event); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart new file mode 100644 index 0000000000..5dd8c3dad0 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart @@ -0,0 +1,69 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('Enter key without shift handler', () { + testWidgets('Pressing enter key in empty document', (tester) async { + final editor = tester.editor + ..initialize() + ..insertEmptyTextNode(); + await editor.startTesting(); + await editor.updateSelection( + Selection.collapsed( + Position(path: [0], offset: 0), + ), + ); + // Pressing the enter key continuously. + for (int i = 1; i <= 10; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect(editor.documentLength, i + 1); + expect(editor.documentSelection, + Selection.collapsed(Position(path: [i], offset: 0))); + } + }); + + testWidgets('Pressing enter key in non-empty document', (tester) async { + const text = 'Welcome to Appflowy 😁'; + var lines = 5; + + final editor = tester.editor..initialize(); + for (var i = 1; i <= lines; i++) { + editor.insertTextNode(text: text); + } + await editor.startTesting(); + + expect(editor.documentLength, lines); + + // Pressing the enter key in last line. + await editor.updateSelection( + Selection.collapsed( + Position(path: [lines - 1], offset: 0), + ), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + lines += 1; + + expect(editor.documentLength, lines); + expect(editor.documentSelection, + Selection.collapsed(Position(path: [lines - 1], offset: 0))); + var lastNode = editor.nodeAtPath([lines - 1]); + expect(lastNode != null, true); + expect(lastNode is TextNode, true); + lastNode = lastNode as TextNode; + expect(lastNode.delta.toRawString(), text); + expect((lastNode.previous as TextNode).delta.toRawString(), ''); + expect( + (lastNode.previous!.previous as TextNode).delta.toRawString(), text); + }); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart b/frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart new file mode 100644 index 0000000000..1291778a56 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../infra/test_raw_key_event.dart'; + +void main() async { + final file = File('test_assets/example.json'); + final json = jsonDecode(await file.readAsString()); + print(json); + + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + testWidgets('init FlowyEditor ', (tester) async { + final editorState = EditorState( + document: StateTree.fromJson(json), + ); + final flowyEditor = FlowyEditor(editorState: editorState); + await tester.pumpWidget(MaterialApp( + home: flowyEditor, + )); + editorState.service.selectionService + .updateSelection(Selection.collapsed(Position(path: [0], offset: 1))); + await tester.pumpAndSettle(); + final key = const TestRawKeyEventData( + logicalKey: LogicalKeyboardKey.enter, + physicalKey: PhysicalKeyboardKey.enter, + ).toKeyEvent; + editorState.service.keyboardService!.onKey(key); + await tester.pumpAndSettle(); + expect(editorState.document.root.children.length, 2); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart new file mode 100644 index 0000000000..19570ee0f8 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -0,0 +1,107 @@ +import 'dart:collection'; + +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_raw_key_event.dart'; + +class EditorWidgetTester { + EditorWidgetTester({ + required this.tester, + }); + + final WidgetTester tester; + late EditorState _editorState; + + EditorState get editorState => _editorState; + Node get root => _editorState.document.root; + + int get documentLength => _editorState.document.root.children.length; + Selection? get documentSelection => + _editorState.service.selectionService.currentSelection.value; + + Future startTesting() async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FlowyEditor( + editorState: _editorState, + ), + ), + ), + ); + return this; + } + + void initialize() { + _editorState = _createEmptyDocument(); + } + + insert(T node) { + _editorState.document.root.insert(node); + } + + insertEmptyTextNode() { + insert(TextNode.empty()); + } + + insertTextNode({String? text, Attributes? attributes}) { + insert( + TextNode( + type: 'text', + delta: Delta( + [TextInsert(text ?? 'Test')], + ), + attributes: attributes, + ), + ); + } + + Node? nodeAtPath(Path path) { + return root.childAtPath(path); + } + + Future updateSelection(Selection? selection) async { + if (selection == null) { + _editorState.service.selectionService.clearSelection(); + } else { + _editorState.service.selectionService.updateSelection(selection); + } + await tester.pumpAndSettle(); + } + + Future pressLogicKey(LogicalKeyboardKey key) async { + late RawKeyEvent testRawKeyEventData; + if (key == LogicalKeyboardKey.enter) { + testRawKeyEventData = const TestRawKeyEventData( + logicalKey: LogicalKeyboardKey.enter, + physicalKey: PhysicalKeyboardKey.enter, + ).toKeyEvent; + } + _editorState.service.keyboardService!.onKey(testRawKeyEventData); + await tester.pumpAndSettle(); + } + + Node _createEmptyEditorRoot() { + return Node( + type: 'editor', + children: LinkedList(), + attributes: {}, + ); + } + + EditorState _createEmptyDocument() { + return EditorState( + document: StateTree( + root: _createEmptyEditorRoot(), + ), + )..disableSealTimer = true; + } +} + +extension TestEditorExtension on WidgetTester { + EditorWidgetTester get editor => EditorWidgetTester(tester: this); + EditorState get editorState => editor.editorState; +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart new file mode 100644 index 0000000000..2cb3fa0fd9 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -0,0 +1,52 @@ +import 'package:flutter/services.dart'; + +class TestRawKeyEvent extends RawKeyDownEvent { + const TestRawKeyEvent({required super.data}); +} + +class TestRawKeyEventData extends RawKeyEventData { + const TestRawKeyEventData({ + required this.logicalKey, + required this.physicalKey, + this.isControlPressed = false, + this.isShiftPressed = false, + this.isAltPressed = false, + this.isMetaPressed = false, + }); + + @override + final bool isControlPressed; + + @override + final bool isShiftPressed; + + @override + final bool isAltPressed; + + @override + final bool isMetaPressed; + + @override + final LogicalKeyboardKey logicalKey; + + @override + final PhysicalKeyboardKey physicalKey; + + @override + KeyboardSide? getModifierSide(ModifierKey key) { + throw UnimplementedError(); + } + + @override + bool isModifierPressed(ModifierKey key, + {KeyboardSide side = KeyboardSide.any}) { + throw UnimplementedError(); + } + + @override + String get keyLabel => throw UnimplementedError(); + + RawKeyEvent get toKeyEvent { + return TestRawKeyEvent(data: this); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/delta_test.dart b/frontend/app_flowy/packages/flowy_editor/test/legacy/delta_test.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/test/delta_test.dart rename to frontend/app_flowy/packages/flowy_editor/test/legacy/delta_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/legacy/flowy_editor_test.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart rename to frontend/app_flowy/packages/flowy_editor/test/legacy/flowy_editor_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/legacy/operation_test.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/test/operation_test.dart rename to frontend/app_flowy/packages/flowy_editor/test/legacy/operation_test.dart From e508d7414c3ca72043a3739cd2d19986d2ad72db Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 13 Aug 2022 15:57:24 +0800 Subject: [PATCH 125/224] chore: delete legacy test file --- ...thout_shift_in_text_node_handler_test.dart | 2 +- .../flowy_editor/test/editor/widget_test.dart | 39 ------------------- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart index 5dd8c3dad0..661862416b 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart @@ -52,7 +52,7 @@ void main() async { LogicalKeyboardKey.enter, ); lines += 1; - + await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(editor.documentLength, lines); expect(editor.documentSelection, Selection.collapsed(Position(path: [lines - 1], offset: 0))); diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart b/frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart deleted file mode 100644 index 1291778a56..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/test/editor/widget_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../infra/test_raw_key_event.dart'; - -void main() async { - final file = File('test_assets/example.json'); - final json = jsonDecode(await file.readAsString()); - print(json); - - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - testWidgets('init FlowyEditor ', (tester) async { - final editorState = EditorState( - document: StateTree.fromJson(json), - ); - final flowyEditor = FlowyEditor(editorState: editorState); - await tester.pumpWidget(MaterialApp( - home: flowyEditor, - )); - editorState.service.selectionService - .updateSelection(Selection.collapsed(Position(path: [0], offset: 1))); - await tester.pumpAndSettle(); - final key = const TestRawKeyEventData( - logicalKey: LogicalKeyboardKey.enter, - physicalKey: PhysicalKeyboardKey.enter, - ).toKeyEvent; - editorState.service.keyboardService!.onKey(key); - await tester.pumpAndSettle(); - expect(editorState.document.root.children.length, 2); - }); -} From d6f1593a20b4a5dfbcb758ffc9144ed51b8b4a80 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sat, 13 Aug 2022 22:37:46 +0800 Subject: [PATCH 126/224] test: implement enter key test for styled text --- .../src/service/render_plugin_service.dart | 3 +- .../lib/src/service/selection_service.dart | 4 +- ...thout_shift_in_text_node_handler_test.dart | 110 ++++++++++++++++-- .../flowy_editor/test/infra/test_editor.dart | 9 +- 4 files changed, 108 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart index e973d16d6a..b6f9414a9e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart @@ -77,7 +77,8 @@ class FlowyRenderPlugin extends FlowyRenderPluginService { node.key = key; return _autoUpdateNodeWidget(builder, context); } else { - assert(false, 'Could not query the builder with this $name'); + assert(false, + 'Could not query the builder with this $name, or nodeValidator return false.'); // TODO: return a placeholder widget with tips. return Container(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index ecf013d0a4..a256d71f03 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -187,11 +187,11 @@ class _FlowySelectionState extends State if (selection != null) { if (selection.isCollapsed) { /// updates cursor area. - debugPrint('updating cursor'); + debugPrint('updating cursor, $selection'); _updateCursorAreas(selection.start); } else { // updates selection area. - debugPrint('updating selection'); + debugPrint('updating selection, $selection'); _updateSelectionAreas(selection); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart index 661862416b..dd722ec0d0 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart @@ -1,4 +1,5 @@ import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; @@ -8,11 +9,9 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); }); - group('Enter key without shift handler', () { - testWidgets('Pressing enter key in empty document', (tester) async { - final editor = tester.editor - ..initialize() - ..insertEmptyTextNode(); + group('enter_without_shift_in_text_node_handler.dart', () { + testWidgets('Presses enter key in empty document', (tester) async { + final editor = tester.editor..insertEmptyTextNode(); await editor.startTesting(); await editor.updateSelection( Selection.collapsed( @@ -30,19 +29,32 @@ void main() async { } }); - testWidgets('Pressing enter key in non-empty document', (tester) async { + testWidgets('Presses enter key in non-empty document', (tester) async { + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // [Empty Line] + // Welcome to Appflowy 😁 + // const text = 'Welcome to Appflowy 😁'; - var lines = 5; + var lines = 3; - final editor = tester.editor..initialize(); + final editor = tester.editor; for (var i = 1; i <= lines; i++) { - editor.insertTextNode(text: text); + editor.insertTextNode(text); } await editor.startTesting(); expect(editor.documentLength, lines); - // Pressing the enter key in last line. + // Presses the enter key in last line. await editor.updateSelection( Selection.collapsed( Position(path: [lines - 1], offset: 0), @@ -52,7 +64,6 @@ void main() async { LogicalKeyboardKey.enter, ); lines += 1; - await tester.pumpAndSettle(const Duration(microseconds: 500)); expect(editor.documentLength, lines); expect(editor.documentSelection, Selection.collapsed(Position(path: [lines - 1], offset: 0))); @@ -60,10 +71,87 @@ void main() async { expect(lastNode != null, true); expect(lastNode is TextNode, true); lastNode = lastNode as TextNode; + for (final node in editor.root.children) { + print( + 'path = ${node.path}, text = ${(node as TextNode).toRawString()}'); + } expect(lastNode.delta.toRawString(), text); expect((lastNode.previous as TextNode).delta.toRawString(), ''); expect( (lastNode.previous!.previous as TextNode).delta.toRawString(), text); }); + + // Before + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // [Empty Line] + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] + testWidgets('Presses enter key in bulleted list', (tester) async { + await _testStyleNeedToBeCopy(tester, StyleKey.bulletedList); + }); + testWidgets('Presses enter key in numbered list', (tester) async { + await _testStyleNeedToBeCopy(tester, StyleKey.numberList); + }); + testWidgets('Presses enter key in checkbox styled text', (tester) async { + await _testStyleNeedToBeCopy(tester, StyleKey.checkbox); + }); + testWidgets('Presses enter key in quoted text', (tester) async { + await _testStyleNeedToBeCopy(tester, StyleKey.quote); + }); }); } + +Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { + const text = 'Welcome to Appflowy 😁'; + Attributes attributes = { + StyleKey.subtype: style, + }; + if (style == StyleKey.checkbox) { + attributes[StyleKey.checkbox] = false; + } else if (style == StyleKey.numberList) { + attributes[StyleKey.number] = 1; + } + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text, attributes: attributes) + ..insertTextNode(text, attributes: attributes); + + await editor.startTesting(); + await editor.updateSelection( + Selection.collapsed( + Position(path: [1], offset: 0), + ), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect(editor.documentSelection, + Selection.collapsed(Position(path: [2], offset: 0))); + + await editor.updateSelection( + Selection.collapsed( + Position(path: [3], offset: text.length), + ), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect(editor.documentSelection, + Selection.collapsed(Position(path: [4], offset: 0))); + expect(editor.nodeAtPath([4])?.subtype, style); + + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect(editor.documentSelection, + Selection.collapsed(Position(path: [4], offset: 0))); + expect(editor.nodeAtPath([4])?.subtype, null); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index 19570ee0f8..3f780412c4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -39,15 +39,15 @@ class EditorWidgetTester { _editorState = _createEmptyDocument(); } - insert(T node) { + void insert(T node) { _editorState.document.root.insert(node); } - insertEmptyTextNode() { + void insertEmptyTextNode() { insert(TextNode.empty()); } - insertTextNode({String? text, Attributes? attributes}) { + void insertTextNode(String? text, {Attributes? attributes}) { insert( TextNode( type: 'text', @@ -102,6 +102,7 @@ class EditorWidgetTester { } extension TestEditorExtension on WidgetTester { - EditorWidgetTester get editor => EditorWidgetTester(tester: this); + EditorWidgetTester get editor => + EditorWidgetTester(tester: this)..initialize(); EditorState get editorState => editor.editorState; } From 07ab4c2680efccea83411c318b85edcb487d867d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 14 Aug 2022 12:39:37 +0800 Subject: [PATCH 127/224] test: add flowy_editor_test into github workflows --- .github/workflows/dart_test.yml | 6 --- .github/workflows/flowy_editor_test.yml | 37 +++++++++++++++++++ ...er_without_shift_in_text_node_handler.dart | 2 +- ...thout_shift_in_text_node_handler_test.dart | 4 -- 4 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/flowy_editor_test.yml diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml index 3ff581c2a3..33cbeb1a3a 100644 --- a/.github/workflows/dart_test.yml +++ b/.github/workflows/dart_test.yml @@ -78,9 +78,3 @@ jobs: run: | flutter pub get flutter test - - - name: Run FlowyEditor tests - working-directory: frontend/app_flowy/packages/flowy_editor - run: | - flutter pub get - flutter test diff --git a/.github/workflows/flowy_editor_test.yml b/.github/workflows/flowy_editor_test.yml new file mode 100644 index 0000000000..80ac4fe5e5 --- /dev/null +++ b/.github/workflows/flowy_editor_test.yml @@ -0,0 +1,37 @@ +name: FlowyEditor test + +on: + push: + branches: + - "main" + + pull_request: + branches: + - "main" + - "feat/flowy_editor" + +env: + CARGO_TERM_COLOR: always + +jobs: + tests: + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: '3.0.5' + cache: true + + - name: Run FlowyEditor tests + working-directory: frontend/app_flowy/packages/flowy_editor + run: | + flutter pub get + flutter test diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 1a25a2531d..39c74d2eab 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -86,7 +86,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = ); TransactionBuilder(editorState) ..insertNode( - textNode.path.next, + textNode.path, TextNode.empty(), ) ..afterSelection = afterSelection diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart index dd722ec0d0..a92391bffd 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart @@ -71,10 +71,6 @@ void main() async { expect(lastNode != null, true); expect(lastNode is TextNode, true); lastNode = lastNode as TextNode; - for (final node in editor.root.children) { - print( - 'path = ${node.path}, text = ${(node as TextNode).toRawString()}'); - } expect(lastNode.delta.toRawString(), text); expect((lastNode.previous as TextNode).delta.toRawString(), ''); expect( From 556aa8c3df2db40da841a230f85ddc1b5bf435e2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Sun, 14 Aug 2022 12:39:37 +0800 Subject: [PATCH 128/224] test: add flowy_editor_test into github workflows --- .github/workflows/flowy_editor_test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flowy_editor_test.yml b/.github/workflows/flowy_editor_test.yml index 80ac4fe5e5..892ca9899d 100644 --- a/.github/workflows/flowy_editor_test.yml +++ b/.github/workflows/flowy_editor_test.yml @@ -8,7 +8,6 @@ on: pull_request: branches: - "main" - - "feat/flowy_editor" env: CARGO_TERM_COLOR: always @@ -26,8 +25,8 @@ jobs: - uses: subosito/flutter-action@v2 with: - channel: 'stable' - flutter-version: '3.0.5' + channel: "stable" + flutter-version: "3.0.5" cache: true - name: Run FlowyEditor tests From b37202867114f5b61c82cb91209befa010d55a1a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 10:32:42 +0800 Subject: [PATCH 129/224] chore: Keep the test directory and code directory structure consistent --- .../enter_without_shift_in_text_node_handler_test.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/app_flowy/packages/flowy_editor/test/{editor/key_event_tests => service/internal_key_event_handlers}/enter_without_shift_in_text_node_handler_test.dart (100%) diff --git a/frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/test/editor/key_event_tests/enter_without_shift_in_text_node_handler_test.dart rename to frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart From c7b0ddfa1dffff91e30450da1d329e9feed73bf8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 11:14:33 +0800 Subject: [PATCH 130/224] test: enter key handler test coverage 100% --- ...er_without_shift_in_text_node_handler.dart | 15 +++- ...thout_shift_in_text_node_handler_test.dart | 87 ++++++++++++++----- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index 39c74d2eab..d58147f578 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -24,11 +24,18 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = return KeyEventResult.ignored; } - final nodes = editorState.service.selectionService.currentSelectedNodes; + var selection = editorState.service.selectionService.currentSelection.value; + var nodes = editorState.service.selectionService.currentSelectedNodes; + if (selection == null) { + return KeyEventResult.ignored; + } + if (selection.isForward) { + selection = selection.reversed; + nodes = nodes.reversed.toList(growable: false); + } final textNodes = nodes.whereType().toList(growable: false); - final selection = editorState.service.selectionService.currentSelection.value; - if (selection == null || nodes.length != textNodes.length) { + if (nodes.length != textNodes.length) { return KeyEventResult.ignored; } @@ -36,7 +43,7 @@ FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = if (!selection.isSingle) { final length = textNodes.length; final List subTextNodes = - length >= 3 ? textNodes.sublist(1, textNodes.length - 2) : []; + length >= 3 ? textNodes.sublist(1, textNodes.length - 1) : []; final afterSelection = Selection.collapsed( Position(path: textNodes.first.path.next, offset: 0), ); diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index a92391bffd..99d61ee6a6 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -11,12 +11,18 @@ void main() async { group('enter_without_shift_in_text_node_handler.dart', () { testWidgets('Presses enter key in empty document', (tester) async { + // Before + // + // [Empty Line] + // + // After + // + // [Empty Line] * 10 + // final editor = tester.editor..insertEmptyTextNode(); await editor.startTesting(); await editor.updateSelection( - Selection.collapsed( - Position(path: [0], offset: 0), - ), + Selection.single(path: [0], startOffset: 0), ); // Pressing the enter key continuously. for (int i = 1; i <= 10; i++) { @@ -25,7 +31,7 @@ void main() async { ); expect(editor.documentLength, i + 1); expect(editor.documentSelection, - Selection.collapsed(Position(path: [i], offset: 0))); + Selection.single(path: [i], startOffset: 0)); } }); @@ -56,9 +62,7 @@ void main() async { // Presses the enter key in last line. await editor.updateSelection( - Selection.collapsed( - Position(path: [lines - 1], offset: 0), - ), + Selection.single(path: [lines - 1], startOffset: 0), ); await editor.pressLogicKey( LogicalKeyboardKey.enter, @@ -66,7 +70,7 @@ void main() async { lines += 1; expect(editor.documentLength, lines); expect(editor.documentSelection, - Selection.collapsed(Position(path: [lines - 1], offset: 0))); + Selection.single(path: [lines - 1], startOffset: 0)); var lastNode = editor.nodeAtPath([lines - 1]); expect(lastNode != null, true); expect(lastNode is TextNode, true); @@ -102,6 +106,16 @@ void main() async { testWidgets('Presses enter key in quoted text', (tester) async { await _testStyleNeedToBeCopy(tester, StyleKey.quote); }); + + testWidgets('Presses enter key in multiple selection from top to bottom', + (tester) async { + _testMultipleSelection(tester, true); + }); + + testWidgets('Presses enter key in multiple selection from bottom to top', + (tester) async { + _testMultipleSelection(tester, false); + }); }); } @@ -111,7 +125,7 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { StyleKey.subtype: style, }; if (style == StyleKey.checkbox) { - attributes[StyleKey.checkbox] = false; + attributes[StyleKey.checkbox] = true; } else if (style == StyleKey.numberList) { attributes[StyleKey.number] = 1; } @@ -122,32 +136,63 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { await editor.startTesting(); await editor.updateSelection( - Selection.collapsed( - Position(path: [1], offset: 0), - ), + Selection.single(path: [1], startOffset: 0), ); await editor.pressLogicKey( LogicalKeyboardKey.enter, ); - expect(editor.documentSelection, - Selection.collapsed(Position(path: [2], offset: 0))); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); await editor.updateSelection( - Selection.collapsed( - Position(path: [3], offset: text.length), - ), + Selection.single(path: [3], startOffset: text.length), ); await editor.pressLogicKey( LogicalKeyboardKey.enter, ); - expect(editor.documentSelection, - Selection.collapsed(Position(path: [4], offset: 0))); + expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); expect(editor.nodeAtPath([4])?.subtype, style); await editor.pressLogicKey( LogicalKeyboardKey.enter, ); - expect(editor.documentSelection, - Selection.collapsed(Position(path: [4], offset: 0))); + expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); expect(editor.nodeAtPath([4])?.subtype, null); } + +Future _testMultipleSelection( + WidgetTester tester, bool isBackwardSelection) async { + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome + // to Appflowy 😁 + // + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + var lines = 4; + + for (var i = 1; i <= lines; i++) { + editor.insertTextNode(text); + } + + await editor.startTesting(); + final start = Position(path: [0], offset: 7); + final end = Position(path: [3], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start, + )); + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + + expect(editor.documentLength, 2); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), 'Welcome'); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), 'to Appflowy 😁'); +} From c008f3369cf532a07243aea54959d2dc5d5a591e Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 12:13:02 +0800 Subject: [PATCH 131/224] test: implement backspace key test for styled text --- .../example/test/widget_test.dart | 24 +---- .../delete_text_handler.dart | 4 +- .../flowy_editor/test/infra/test_editor.dart | 8 +- .../test/infra/test_raw_key_event.dart | 15 ++- .../delete_text_handler_test.dart | 97 +++++++++++++++++++ 5 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart index 092d222f7e..2a2b819285 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/test/widget_test.dart @@ -5,26 +5,4 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} +void main() {} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index 92f5e9afb9..afef57cceb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -72,7 +72,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { _deleteNodes(transactionBuilder, textNodes, selection); } - transactionBuilder.commit(); + if (transactionBuilder.operations.isNotEmpty) { + transactionBuilder.commit(); + } return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index 3f780412c4..b6aebeaa41 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -73,13 +73,7 @@ class EditorWidgetTester { } Future pressLogicKey(LogicalKeyboardKey key) async { - late RawKeyEvent testRawKeyEventData; - if (key == LogicalKeyboardKey.enter) { - testRawKeyEventData = const TestRawKeyEventData( - logicalKey: LogicalKeyboardKey.enter, - physicalKey: PhysicalKeyboardKey.enter, - ).toKeyEvent; - } + final testRawKeyEventData = TestRawKeyEventData(logicalKey: key).toKeyEvent; _editorState.service.keyboardService!.onKey(testRawKeyEventData); await tester.pumpAndSettle(); } diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index 2cb3fa0fd9..aa98781d7d 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -7,7 +7,6 @@ class TestRawKeyEvent extends RawKeyDownEvent { class TestRawKeyEventData extends RawKeyEventData { const TestRawKeyEventData({ required this.logicalKey, - required this.physicalKey, this.isControlPressed = false, this.isShiftPressed = false, this.isAltPressed = false, @@ -30,7 +29,7 @@ class TestRawKeyEventData extends RawKeyEventData { final LogicalKeyboardKey logicalKey; @override - final PhysicalKeyboardKey physicalKey; + PhysicalKeyboardKey get physicalKey => logicalKey.toPhysicalKey; @override KeyboardSide? getModifierSide(ModifierKey key) { @@ -50,3 +49,15 @@ class TestRawKeyEventData extends RawKeyEventData { return TestRawKeyEvent(data: this); } } + +extension on LogicalKeyboardKey { + PhysicalKeyboardKey get toPhysicalKey { + if (this == LogicalKeyboardKey.enter) { + return PhysicalKeyboardKey.enter; + } + if (this == LogicalKeyboardKey.backspace) { + return PhysicalKeyboardKey.backspace; + } + throw UnimplementedError(); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart new file mode 100644 index 0000000000..4a42ac2c47 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart @@ -0,0 +1,97 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('delete_text_handler.dart', () { + testWidgets('Presses backspace key in empty document', (tester) async { + // Before + // + // [Empty Line] + // + // After + // + // [Empty Line] + // + final editor = tester.editor..insertEmptyTextNode(); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + // Pressing the backspace key continuously. + for (int i = 1; i <= 1; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentLength, 1); + expect(editor.documentSelection, + Selection.single(path: [0], startOffset: 0)); + } + }); + }); + + // Before + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁Welcome to Appflowy 😁 + // + testWidgets('Presses backspace key in styled text', (tester) async { + await _deleteStyledText(tester, StyleKey.checkbox); + }); +} + +Future _deleteStyledText(WidgetTester tester, String style) async { + const text = 'Welcome to Appflowy 😁'; + Attributes attributes = { + StyleKey.subtype: style, + }; + if (style == StyleKey.checkbox) { + attributes[StyleKey.checkbox] = true; + } else if (style == StyleKey.numberList) { + attributes[StyleKey.number] = 1; + } + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text, attributes: attributes) + ..insertTextNode(text, attributes: attributes); + + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [2], startOffset: 0), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 0)); + + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, + Selection.single(path: [1], startOffset: text.length)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text * 2); + + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0), + ); + await editor.pressLogicKey( + LogicalKeyboardKey.backspace, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, null); +} From 8c2cca7d7b23d78f1f889a46c625fe8ee2c4edfb Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 12:17:45 +0800 Subject: [PATCH 132/224] feat: copy h1/h2/h3 styles --- .../lib/src/infra/html_converter.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 4a15b54859..0f8578b5c7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -427,8 +427,10 @@ class NodesToHTMLConverter { html.Element _textNodeToHtml(TextNode textNode, {int? end}) { String? subType = textNode.attributes["subtype"]; + String? heading = textNode.attributes["heading"]; return _deltaToHtml(textNode.delta, subType: subType, + heading: heading, end: end, checked: textNode.attributes["checkbox"] == true); } @@ -501,7 +503,7 @@ class NodesToHTMLConverter { /// Text /// ``` html.Element _deltaToHtml(Delta delta, - {String? subType, int? end, bool? checked}) { + {String? subType, String? heading, int? end, bool? checked}) { if (end != null) { delta = delta.slice(0, end); } @@ -517,6 +519,14 @@ class NodesToHTMLConverter { node.attributes["checked"] = "true"; } childNodes.add(node); + } else if (subType == StyleKey.heading) { + if (heading == StyleKey.h1) { + tagName = tagH1; + } else if (heading == StyleKey.h2) { + tagName = tagH2; + } else if (heading == StyleKey.h3) { + tagName = tagH3; + } } for (final op in delta) { @@ -557,7 +567,10 @@ class NodesToHTMLConverter { } } - if (tagName != tagParagraph) { + if (tagName != tagParagraph && + tagName != tagH1 && + tagName != tagH2 && + tagName != tagH3) { final p = html.Element.tag(tagParagraph); for (final node in childNodes) { p.append(node); From 81d4c8d23bc7b69615574209c8615854c75a5ec1 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 12:51:32 +0800 Subject: [PATCH 133/224] feat: blockquote --- .../lib/src/infra/html_converter.dart | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 0f8578b5c7..5963d5955d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -24,6 +24,7 @@ const String tagDel = "del"; const String tagStrong = "strong"; const String tagSpan = "span"; const String tagCode = "code"; +const String tagBlockQuote = "blockquote"; extension on Color { String toRgbaString() { @@ -70,6 +71,8 @@ class HTMLToNodesConverter { } else { result.add(_handleRichText(child)); } + } else if (child.localName == tagBlockQuote) { + result.addAll(_handleBlockQuote(child)); } else { result.addAll(_handleElement(child)); } @@ -83,6 +86,18 @@ class HTMLToNodesConverter { return result; } + List _handleBlockQuote(html.Element element) { + final result = []; + + for (final child in element.nodes.toList()) { + if (child is html.Element) { + result.addAll(_handleElement(child, {"subtype": "quote"})); + } + } + + return result; + } + List _handleBTag(html.Element element) { final childNodes = element.nodes; return _handleContainer(childNodes); @@ -527,6 +542,8 @@ class NodesToHTMLConverter { } else if (heading == StyleKey.h3) { tagName = tagH3; } + } else if (subType == StyleKey.quote) { + tagName = tagBlockQuote; } for (final op in delta) { @@ -567,10 +584,19 @@ class NodesToHTMLConverter { } } - if (tagName != tagParagraph && + if (tagName == tagBlockQuote) { + final p = html.Element.tag(tagParagraph); + for (final node in childNodes) { + p.append(node); + } + final blockQuote = html.Element.tag(tagName); + blockQuote.append(p); + return blockQuote; + } else if (tagName != tagParagraph && tagName != tagH1 && tagName != tagH2 && - tagName != tagH3) { + tagName != tagH3 && + tagName != tagBlockQuote) { final p = html.Element.tag(tagParagraph); for (final node in childNodes) { p.append(node); From 0def9e888265366bc4da900f850a19d12f932986 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 13:58:36 +0800 Subject: [PATCH 134/224] feat: remove document.json from repo --- .../flowy_editor/example/assets/document.json | 245 ------------------ .../flowy_editor/example/pubspec.yaml | 5 +- .../packages/flowy_editor/pubspec.yaml | 1 - 3 files changed, 1 insertion(+), 250 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/assets/document.json diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json deleted file mode 100644 index 307b4bf92f..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "document": { - "type": "editor", - "attributes": {}, - "children": [ - { - "type": "image", - "attributes": { - "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "👋 Welcome to AppFlowy!", - "attributes": { - "href": "https://www.appflowy.io/", - "heading": "h1" - } - } - ], - "attributes": { - "heading": "h1" - } - }, - { - "type": "text", - "delta": [ - { "insert": "Here are the basics", "attributes": { "heading": "h2" } } - ], - "attributes": { - "heading": "h2" - } - }, - { - "type": "text", - "delta": [{ "insert": "Click anywhere and just start typing." }], - "attributes": { - "list": "todo", - "todo": false - } - }, - { - "type": "text", - "delta": [{ "insert": "Click anywhere and just start typing." }], - "attributes": { - "list": "bullet" - } - }, - { - "type": "text", - "delta": [{ "insert": "Click anywhere and just start typing." }], - "attributes": { - "list": "bullet" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "Highlight", - "attributes": { "highlight": "0xFFFFFF00" } - }, - { "insert": " Click anywhere and just start typing" }, - { "insert": " any text, and use the menu at the bottom to " }, - { "insert": "style", "attributes": { "italic": true } }, - { "insert": " your ", "attributes": { "bold": true } }, - { "insert": "writing", "attributes": { "underline": true } }, - { - "insert": " however you like.", - "attributes": { "strikethrough": true } - } - ], - "attributes": { - "checkbox": false - } - }, - { - "type": "text", - "delta": [ - { "insert": "Have a question? ", "attributes": { "heading": "h2" } } - ], - "attributes": { - "heading": "h2" - } - }, - { - "type": "text", - "delta": [ - { - "insert": "1. Click the '?' at the bottom right for help and support." - } - ], - "attributes": { - "quotes": true - } - }, - { - "type": "text", - "delta": [ - { - "insert": "2. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "3. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "4. Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "5. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "6. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "7. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "8. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "9. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "10. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "11. Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - }, - { - "type": "text", - "delta": [ - { - "insert": "Click the '?' at the bottom right for help and support." - } - ], - "attributes": {} - } - ] - } -} diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml index 65f15eae46..a9b374af5c 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/example/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -30,7 +30,6 @@ dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 @@ -58,7 +57,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. @@ -66,7 +64,6 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - document.json - example.json - big_document.json # - images/a_dot_ham.jpeg diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 05c87f8e33..24021ec252 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -31,7 +31,6 @@ flutter: - assets/images/toolbar/ - assets/images/popup_list/ - assets/images/ - - assets/document.json # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From fe7c19666f873967382e346bc2304a535ab255d2 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 13:58:46 +0800 Subject: [PATCH 135/224] feat: add converter --- .../packages/flowy_editor/lib/src/infra/html_converter.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 5963d5955d..329544d0cc 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -91,7 +91,7 @@ class HTMLToNodesConverter { for (final child in element.nodes.toList()) { if (child is html.Element) { - result.addAll(_handleElement(child, {"subtype": "quote"})); + result.addAll(_handleElement(child, {"subtype": StyleKey.quote})); } } From e34ff509233e4b933e89ba7e41dc079ebbc4c284 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 14:03:38 +0800 Subject: [PATCH 136/224] fix: CI errors --- frontend/app_flowy/packages/flowy_editor/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 24021ec252..05c87f8e33 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -31,6 +31,7 @@ flutter: - assets/images/toolbar/ - assets/images/popup_list/ - assets/images/ + - assets/document.json # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # From c66e1e4df84e95d70b98278ce1346b7c9cd05d0d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 14:05:18 +0800 Subject: [PATCH 137/224] test: implement delete key test for styled text --- .../packages/flowy_editor/coverage/lcov.info | 0 .../delete_text_handler.dart | 6 +- .../flowy_editor/test/infra/test_editor.dart | 9 + .../test/infra/test_raw_key_event.dart | 3 + .../delete_text_handler_test.dart | 260 +++++++++++++++++- 5 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/coverage/lcov.info diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart index afef57cceb..ccddff27be 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart @@ -80,11 +80,13 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { - final selection = editorState.service.selectionService.currentSelection.value; + var selection = editorState.service.selectionService.currentSelection.value; if (selection == null) { return KeyEventResult.ignored; } - final nodes = editorState.service.selectionService.currentSelectedNodes; + var nodes = editorState.service.selectionService.currentSelectedNodes; + nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); + selection = selection.isBackward ? selection : selection.reversed; // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); if (textNodes.length != nodes.length) { diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index b6aebeaa41..ddbe4d5b2c 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -95,6 +95,15 @@ class EditorWidgetTester { } } +extension TestString on String { + String safeSubString([int start = 0, int? end]) { + end ??= length - 1; + end = end.clamp(start, length - 1); + final sRunes = runes; + return String.fromCharCodes(sRunes, start, end); + } +} + extension TestEditorExtension on WidgetTester { EditorWidgetTester get editor => EditorWidgetTester(tester: this)..initialize(); diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index aa98781d7d..48c4ab3e67 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -58,6 +58,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.backspace) { return PhysicalKeyboardKey.backspace; } + if (this == LogicalKeyboardKey.delete) { + return PhysicalKeyboardKey.delete; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart index 4a42ac2c47..15af27e7a4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart @@ -36,6 +36,87 @@ void main() async { }); }); + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome Appflowy 😁 + // + // Then + // Welcome to Appflowy 😁 + // + testWidgets( + 'Presses backspace key in non-empty document and selection is backward', + (tester) async { + await _deleteTextByBackspace(tester, true); + }); + testWidgets( + 'Presses backspace key in non-empty document and selection is forward', + (tester) async { + await _deleteTextByBackspace(tester, false); + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome Appflowy 😁 + // + // Then + // Welcome to Appflowy 😁 + // + testWidgets( + 'Presses delete key in non-empty document and selection is backward', + (tester) async { + await _deleteTextByDelete(tester, true); + }); + testWidgets( + 'Presses delete key in non-empty document and selection is forward', + (tester) async { + await _deleteTextByDelete(tester, false); + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁Welcome Appflowy 😁 + testWidgets( + 'Presses delete key in non-empty document and selection is at the end of the text', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [0], startOffset: text.length), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + + expect(editor.documentLength, 1); + expect(editor.documentSelection, + Selection.single(path: [0], startOffset: text.length)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), text * 2); + }); + // Before // // Welcome to Appflowy 😁 @@ -47,12 +128,49 @@ void main() async { // Welcome to Appflowy 😁 // [Style] Welcome to Appflowy 😁Welcome to Appflowy 😁 // - testWidgets('Presses backspace key in styled text', (tester) async { - await _deleteStyledText(tester, StyleKey.checkbox); + testWidgets('Presses backspace key in styled text (checkbox)', + (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.checkbox); + }); + testWidgets('Presses backspace key in styled text (bulletedList)', + (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.bulletedList); + }); + testWidgets('Presses backspace key in styled text (heading)', (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.heading); + }); + testWidgets('Presses backspace key in styled text (quote)', (tester) async { + await _deleteStyledTextByBackspace(tester, StyleKey.quote); + }); + + // Before + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // [Style] Welcome to Appflowy 😁 + // + testWidgets('Presses delete key in styled text (checkbox)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.checkbox); + }); + testWidgets('Presses delete key in styled text (bulletedList)', + (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.bulletedList); + }); + testWidgets('Presses delete key in styled text (heading)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.heading); + }); + testWidgets('Presses delete key in styled text (quote)', (tester) async { + await _deleteStyledTextByDelete(tester, StyleKey.quote); }); } -Future _deleteStyledText(WidgetTester tester, String style) async { +Future _deleteStyledTextByBackspace( + WidgetTester tester, String style) async { const text = 'Welcome to Appflowy 😁'; Attributes attributes = { StyleKey.subtype: style, @@ -61,6 +179,8 @@ Future _deleteStyledText(WidgetTester tester, String style) async { attributes[StyleKey.checkbox] = true; } else if (style == StyleKey.numberList) { attributes[StyleKey.number] = 1; + } else if (style == StyleKey.heading) { + attributes[StyleKey.heading] = StyleKey.h1; } final editor = tester.editor ..insertTextNode(text) @@ -95,3 +215,137 @@ Future _deleteStyledText(WidgetTester tester, String style) async { expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); expect(editor.nodeAtPath([1])?.subtype, null); } + +Future _deleteStyledTextByDelete( + WidgetTester tester, String style) async { + const text = 'Welcome to Appflowy 😁'; + Attributes attributes = { + StyleKey.subtype: style, + }; + if (style == StyleKey.checkbox) { + attributes[StyleKey.checkbox] = true; + } else if (style == StyleKey.numberList) { + attributes[StyleKey.number] = 1; + } else if (style == StyleKey.heading) { + attributes[StyleKey.heading] = StyleKey.h1; + } + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text, attributes: attributes) + ..insertTextNode(text, attributes: attributes); + + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0), + ); + for (var i = 1; i < text.length; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.delete, + ); + expect( + editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + text.safeSubString(i)); + } + + await editor.pressLogicKey( + LogicalKeyboardKey.delete, + ); + expect(editor.documentLength, 2); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 0)); + expect(editor.nodeAtPath([1])?.subtype, style); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); +} + +Future _deleteTextByBackspace( + WidgetTester tester, bool isBackwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [1], startOffset: 10), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome t Appflowy 😁'); + + // delete 'to ' + await editor.updateSelection( + Selection.single(path: [2], startOffset: 8, endOffset: 11), + ); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); + expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + 'Welcome Appflowy 😁'); + + // delete 'Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome ' + final start = Position(path: [0], offset: 11); + final end = Position(path: [2], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start)); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 1); + expect( + editor.documentSelection, Selection.single(path: [0], startOffset: 11)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); +} + +Future _deleteTextByDelete( + WidgetTester tester, bool isBackwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + // delete 'o' + await editor.updateSelection( + Selection.single(path: [1], startOffset: 9), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [1], startOffset: 9)); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), + 'Welcome t Appflowy 😁'); + + // delete 'to ' + await editor.updateSelection( + Selection.single(path: [2], startOffset: 8, endOffset: 11), + ); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + expect(editor.documentLength, 3); + expect(editor.documentSelection, Selection.single(path: [2], startOffset: 8)); + expect((editor.nodeAtPath([2]) as TextNode).toRawString(), + 'Welcome Appflowy 😁'); + + // delete 'Appflowy 😁 + // Welcome t Appflowy 😁 + // Welcome ' + final start = Position(path: [0], offset: 11); + final end = Position(path: [2], offset: 8); + await editor.updateSelection(Selection( + start: isBackwardSelection ? start : end, + end: isBackwardSelection ? end : start)); + await editor.pressLogicKey(LogicalKeyboardKey.delete); + expect(editor.documentLength, 1); + expect( + editor.documentSelection, Selection.single(path: [0], startOffset: 11)); + expect((editor.nodeAtPath([0]) as TextNode).toRawString(), + 'Welcome to Appflowy 😁'); +} From 6d46f40fae341035e8b1328e1fddcabf0c8f0224 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 14:06:56 +0800 Subject: [PATCH 138/224] chore: delete icov.info --- frontend/app_flowy/packages/flowy_editor/coverage/lcov.info | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/coverage/lcov.info diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info deleted file mode 100644 index e69de29bb2..0000000000 From a3ffbe2f00330a3e4e55eac19a87686869a77955 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 14:21:15 +0800 Subject: [PATCH 139/224] refactor: html text into class --- .../lib/src/infra/html_converter.dart | 142 ++++++++++-------- 1 file changed, 76 insertions(+), 66 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 329544d0cc..59efabe364 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'dart:ui'; import 'package:flowy_editor/src/document/attributes.dart'; import 'package:flowy_editor/src/document/node.dart'; @@ -8,23 +9,35 @@ import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; -const String tagH1 = "h1"; -const String tagH2 = "h2"; -const String tagH3 = "h3"; -const String tagOrderedList = "ol"; -const String tagUnorderedList = "ul"; -const String tagList = "li"; -const String tagParagraph = "p"; -const String tagImage = "img"; -const String tagAnchor = "a"; -const String tagItalic = "i"; -const String tagBold = "b"; -const String tagUnderline = "u"; -const String tagDel = "del"; -const String tagStrong = "strong"; -const String tagSpan = "span"; -const String tagCode = "code"; -const String tagBlockQuote = "blockquote"; +class HTMLTag { + static const h1 = "h1"; + static const h2 = "h2"; + static const h3 = "h3"; + static const orderedList = "ol"; + static const unorderedList = "ul"; + static const list = "li"; + static const paragraph = "p"; + static const image = "img"; + static const anchor = "a"; + static const italic = "i"; + static const bold = "b"; + static const underline = "u"; + static const del = "del"; + static const strong = "strong"; + static const span = "span"; + static const code = "code"; + static const blockQuote = "blockquote"; + static const div = "div"; + + static bool isTopLevel(String tag) { + return tag == h1 || + tag == h2 || + tag == h3 || + tag == paragraph || + tag == div || + tag == blockQuote; + } +} extension on Color { String toRgbaString() { @@ -55,15 +68,15 @@ class HTMLToNodesConverter { final result = []; for (final child in childNodes) { if (child is html.Element) { - if (child.localName == tagAnchor || - child.localName == tagSpan || - child.localName == tagCode || - child.localName == tagStrong || - child.localName == tagUnderline || - child.localName == tagItalic || - child.localName == tagDel) { + if (child.localName == HTMLTag.anchor || + child.localName == HTMLTag.span || + child.localName == HTMLTag.code || + child.localName == HTMLTag.strong || + child.localName == HTMLTag.underline || + child.localName == HTMLTag.italic || + child.localName == HTMLTag.del) { _handleRichTextElement(delta, child); - } else if (child.localName == tagBold) { + } else if (child.localName == HTMLTag.bold) { // Google docs wraps the the content inside the `` tag. // It's strange if (!_inParagraph) { @@ -71,7 +84,7 @@ class HTMLToNodesConverter { } else { result.add(_handleRichText(child)); } - } else if (child.localName == tagBlockQuote) { + } else if (child.localName == HTMLTag.blockQuote) { result.addAll(_handleBlockQuote(child)); } else { result.addAll(_handleElement(child)); @@ -105,19 +118,19 @@ class HTMLToNodesConverter { List _handleElement(html.Element element, [Map? attributes]) { - if (element.localName == tagH1) { - return [_handleHeadingElement(element, tagH1)]; - } else if (element.localName == tagH2) { - return [_handleHeadingElement(element, tagH2)]; - } else if (element.localName == tagH3) { - return [_handleHeadingElement(element, tagH3)]; - } else if (element.localName == tagUnorderedList) { + if (element.localName == HTMLTag.h1) { + return [_handleHeadingElement(element, HTMLTag.h1)]; + } else if (element.localName == HTMLTag.h2) { + return [_handleHeadingElement(element, HTMLTag.h2)]; + } else if (element.localName == HTMLTag.h3) { + return [_handleHeadingElement(element, HTMLTag.h3)]; + } else if (element.localName == HTMLTag.unorderedList) { return _handleUnorderedList(element); - } else if (element.localName == tagOrderedList) { + } else if (element.localName == HTMLTag.orderedList) { return _handleOrderedList(element); - } else if (element.localName == tagList) { + } else if (element.localName == HTMLTag.list) { return _handleListElement(element); - } else if (element.localName == tagParagraph) { + } else if (element.localName == HTMLTag.paragraph) { return [_handleParagraph(element, attributes)]; } else { final delta = Delta(); @@ -236,23 +249,24 @@ class HTMLToNodesConverter { } _handleRichTextElement(Delta delta, html.Element element) { - if (element.localName == tagSpan) { + if (element.localName == HTMLTag.span) { delta.insert(element.text, _getDeltaAttributesFromHtmlAttributes(element.attributes)); - } else if (element.localName == tagAnchor) { + } else if (element.localName == HTMLTag.anchor) { final hyperLink = element.attributes["href"]; Map? attributes; if (hyperLink != null) { attributes = {"href": hyperLink}; } delta.insert(element.text, attributes); - } else if (element.localName == tagStrong || element.localName == tagBold) { + } else if (element.localName == HTMLTag.strong || + element.localName == HTMLTag.bold) { delta.insert(element.text, {StyleKey.bold: true}); - } else if (element.localName == tagUnderline) { + } else if (element.localName == HTMLTag.underline) { delta.insert(element.text, {StyleKey.underline: true}); - } else if (element.localName == tagItalic) { + } else if (element.localName == HTMLTag.italic) { delta.insert(element.text, {StyleKey.italic: true}); - } else if (element.localName == tagDel) { + } else if (element.localName == HTMLTag.del) { delta.insert(element.text, {StyleKey.strikethrough: true}); } else { delta.insert(element.text); @@ -265,7 +279,7 @@ class HTMLToNodesConverter { /// A container contains a will be regarded as a image block Node _handleRichText(html.Element element, [Map? attributes]) { - final image = element.querySelector(tagImage); + final image = element.querySelector(HTMLTag.image); if (image != null) { final imageNode = _handleImage(image); return imageNode; @@ -419,10 +433,10 @@ class NodesToHTMLConverter { } _addElement(TextNode textNode, html.Element element) { - if (element.localName == tagList) { + if (element.localName == HTMLTag.list) { final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList; - _stashListContainer ??= - html.Element.tag(isNumbered ? tagOrderedList : tagUnorderedList); + _stashListContainer ??= html.Element.tag( + isNumbered ? HTMLTag.orderedList : HTMLTag.unorderedList); _stashListContainer?.append(element); } else { if (_stashListContainer != null) { @@ -524,10 +538,10 @@ class NodesToHTMLConverter { } final childNodes = []; - String tagName = tagParagraph; + String tagName = HTMLTag.paragraph; if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) { - tagName = tagList; + tagName = HTMLTag.list; } else if (subType == StyleKey.checkbox) { final node = html.Element.html(''); if (checked != null && checked) { @@ -536,14 +550,14 @@ class NodesToHTMLConverter { childNodes.add(node); } else if (subType == StyleKey.heading) { if (heading == StyleKey.h1) { - tagName = tagH1; + tagName = HTMLTag.h1; } else if (heading == StyleKey.h2) { - tagName = tagH2; + tagName = HTMLTag.h2; } else if (heading == StyleKey.h3) { - tagName = tagH3; + tagName = HTMLTag.h3; } } else if (subType == StyleKey.quote) { - tagName = tagBlockQuote; + tagName = HTMLTag.blockQuote; } for (final op in delta) { @@ -551,26 +565,26 @@ class NodesToHTMLConverter { final attributes = op.attributes; if (attributes != null) { if (attributes.length == 1 && attributes[StyleKey.bold] == true) { - final strong = html.Element.tag(tagStrong); + final strong = html.Element.tag(HTMLTag.strong); strong.append(html.Text(op.content)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[StyleKey.underline] == true) { - final strong = html.Element.tag(tagUnderline); + final strong = html.Element.tag(HTMLTag.underline); strong.append(html.Text(op.content)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[StyleKey.italic] == true) { - final strong = html.Element.tag(tagItalic); + final strong = html.Element.tag(HTMLTag.italic); strong.append(html.Text(op.content)); childNodes.add(strong); } else if (attributes.length == 1 && attributes[StyleKey.strikethrough] == true) { - final strong = html.Element.tag(tagDel); + final strong = html.Element.tag(HTMLTag.del); strong.append(html.Text(op.content)); childNodes.add(strong); } else { - final span = html.Element.tag(tagSpan); + final span = html.Element.tag(HTMLTag.span); final cssString = _attributesToCssStyle(attributes); if (cssString.isNotEmpty) { span.attributes["style"] = cssString; @@ -584,24 +598,20 @@ class NodesToHTMLConverter { } } - if (tagName == tagBlockQuote) { - final p = html.Element.tag(tagParagraph); + if (tagName == HTMLTag.blockQuote) { + final p = html.Element.tag(HTMLTag.paragraph); for (final node in childNodes) { p.append(node); } final blockQuote = html.Element.tag(tagName); blockQuote.append(p); return blockQuote; - } else if (tagName != tagParagraph && - tagName != tagH1 && - tagName != tagH2 && - tagName != tagH3 && - tagName != tagBlockQuote) { - final p = html.Element.tag(tagParagraph); + } else if (!HTMLTag.isTopLevel(tagName)) { + final p = html.Element.tag(HTMLTag.paragraph); for (final node in childNodes) { p.append(node); } - final result = html.Element.tag(tagList); + final result = html.Element.tag(HTMLTag.list); result.append(p); return result; } else { From f5cc886b6e5ce45c260d48518132c4a3717b220b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 14:47:54 +0800 Subject: [PATCH 140/224] test: implement pageup/down key test for large document --- .../page_up_down_handler.dart | 13 +--- .../lib/src/service/scroll_service.dart | 28 +++++++ .../test/infra/test_raw_key_event.dart | 6 ++ .../page_up_down_handler_test.dart | 75 +++++++++++++++++++ 4 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart index 5acdcdcb0d..18ab2d23ee 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/page_up_down_handler.dart @@ -2,25 +2,16 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -double? getEditorHeight(EditorState editorState) { - final renderObj = - editorState.service.scrollServiceKey.currentContext?.findRenderObject(); - if (renderObj is RenderBox) { - return renderObj.size.height; - } - return null; -} - FlowyKeyEventHandler pageUpDownHandler = (editorState, event) { if (event.logicalKey == LogicalKeyboardKey.pageUp) { - final scrollHeight = getEditorHeight(editorState); + final scrollHeight = editorState.service.scrollService?.onePageHeight; final scrollService = editorState.service.scrollService; if (scrollHeight != null && scrollService != null) { scrollService.scrollTo(scrollService.dy - scrollHeight); } return KeyEventResult.handled; } else if (event.logicalKey == LogicalKeyboardKey.pageDown) { - final scrollHeight = getEditorHeight(editorState); + final scrollHeight = editorState.service.scrollService?.onePageHeight; final scrollService = editorState.service.scrollService; if (scrollHeight != null && scrollService != null) { scrollService.scrollTo(scrollService.dy + scrollHeight); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart index 5201a18942..199249b715 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart @@ -1,8 +1,15 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_editor/src/extensions/object_extensions.dart'; abstract class FlowyScrollService { double get dy; + double? get onePageHeight; + + int? get page; + + double get maxScrollExtent; + double get minScrollExtent; void scrollTo(double dy); @@ -32,6 +39,27 @@ class _FlowyScrollState extends State @override double get dy => _scrollController.position.pixels; + @override + double? get onePageHeight { + final renderBox = context.findRenderObject()?.unwrapOrNull(); + return renderBox?.size.height; + } + + @override + double get maxScrollExtent => _scrollController.position.maxScrollExtent; + + @override + double get minScrollExtent => _scrollController.position.minScrollExtent; + + @override + int? get page { + if (onePageHeight != null) { + final scrollExtent = maxScrollExtent - minScrollExtent; + return (scrollExtent / onePageHeight!).ceil(); + } + return null; + } + @override Widget build(BuildContext context) { return Listener( diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index 48c4ab3e67..512ba26574 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -61,6 +61,12 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.delete) { return PhysicalKeyboardKey.delete; } + if (this == LogicalKeyboardKey.pageDown) { + return PhysicalKeyboardKey.pageDown; + } + if (this == LogicalKeyboardKey.pageUp) { + return PhysicalKeyboardKey.pageUp; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart new file mode 100644 index 0000000000..fe74e41efe --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/page_up_down_handler_test.dart @@ -0,0 +1,75 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('page_up_down_handler_test.dart', () { + testWidgets('Presses PageUp and pageDown key in large document', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < 1000; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + + final scrollService = editor.editorState.service.scrollService; + + expect(scrollService != null, true); + + if (scrollService == null) { + return; + } + + final page = scrollService.page; + final onePageHeight = scrollService.onePageHeight; + expect(page != null, true); + expect(onePageHeight != null, true); + + // Pressing the pageDown key continuously. + var currentOffsetY = 0.0; + for (int i = 1; i <= page!; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageDown, + ); + currentOffsetY += onePageHeight!; + final dy = scrollService.dy; + expect(dy, currentOffsetY); + } + + for (int i = 1; i <= 5; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageDown, + ); + final dy = scrollService.dy; + expect(dy == scrollService.maxScrollExtent, true); + } + + // Pressing the pageUp key continuously. + for (int i = page; i >= 1; i--) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageUp, + ); + currentOffsetY -= onePageHeight!; + final dy = editor.editorState.service.scrollService?.dy; + expect(dy, currentOffsetY); + } + + for (int i = 1; i <= 5; i++) { + await editor.pressLogicKey( + LogicalKeyboardKey.pageUp, + ); + final dy = scrollService.dy; + expect(dy == scrollService.minScrollExtent, true); + } + }); + }); +} From e826006fa704ef523712518b9c3d3938e4d127d0 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 14:52:29 +0800 Subject: [PATCH 141/224] refactor: color extensions --- .../lib/src/extensions/color_extension.dart | 36 ++++++++++++++++ .../lib/src/infra/html_converter.dart | 43 ++----------------- 2 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/src/extensions/color_extension.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/color_extension.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/color_extension.dart new file mode 100644 index 0000000000..7228127104 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/color_extension.dart @@ -0,0 +1,36 @@ +import 'package:flutter/painting.dart'; + +extension ColorExtension on Color { + /// Try to parse the `rgba(red, greed, blue, alpha)` + /// from the string. + static Color? tryFromRgbaString(String colorString) { + final reg = RegExp(r'rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)'); + final match = reg.firstMatch(colorString); + if (match == null) { + return null; + } + + if (match.groupCount < 4) { + return null; + } + final redStr = match.group(1); + final greenStr = match.group(2); + final blueStr = match.group(3); + final alphaStr = match.group(4); + + final red = redStr != null ? int.tryParse(redStr) : null; + final green = greenStr != null ? int.tryParse(greenStr) : null; + final blue = blueStr != null ? int.tryParse(blueStr) : null; + final alpha = alphaStr != null ? int.tryParse(alphaStr) : null; + + if (red == null || green == null || blue == null || alpha == null) { + return null; + } + + return Color.fromARGB(alpha, red, green, blue); + } + + String toRgbaString() { + return 'rgba($red, $green, $blue, $alpha)'; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 59efabe364..0aa8d4fd13 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -5,6 +5,7 @@ import 'package:flowy_editor/src/document/attributes.dart'; import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/document/text_delta.dart'; import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/extensions/color_extension.dart'; import 'package:flutter/material.dart'; import 'package:html/parser.dart' show parse; import 'package:html/dom.dart' as html; @@ -39,12 +40,6 @@ class HTMLTag { } } -extension on Color { - String toRgbaString() { - return 'rgba($red, $green, $blue, $alpha)'; - } -} - /// Converting the HTML to nodes class HTMLToNodesConverter { final html.Document _document; @@ -192,7 +187,9 @@ class HTMLToNodesConverter { } final backgroundColorStr = cssMap["background-color"]; - final backgroundColor = _tryParseCssColorString(backgroundColorStr); + final backgroundColor = backgroundColorStr == null + ? null + : ColorExtension.tryFromRgbaString(backgroundColorStr); if (backgroundColor != null) { attrs[StyleKey.backgroundColor] = '0x${backgroundColor.value.toRadixString(16)}'; @@ -216,38 +213,6 @@ class HTMLToNodesConverter { } } - /// Try to parse the `rgba(red, greed, blue, alpha)` - /// from the string. - Color? _tryParseCssColorString(String? colorString) { - if (colorString == null) { - return null; - } - final reg = RegExp(r'rgba\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)'); - final match = reg.firstMatch(colorString); - if (match == null) { - return null; - } - - if (match.groupCount < 4) { - return null; - } - final redStr = match.group(1); - final greenStr = match.group(2); - final blueStr = match.group(3); - final alphaStr = match.group(4); - - final red = redStr != null ? int.tryParse(redStr) : null; - final green = greenStr != null ? int.tryParse(greenStr) : null; - final blue = blueStr != null ? int.tryParse(blueStr) : null; - final alpha = alphaStr != null ? int.tryParse(alphaStr) : null; - - if (red == null || green == null || blue == null || alpha == null) { - return null; - } - - return Color.fromARGB(alpha, red, green, blue); - } - _handleRichTextElement(Delta delta, html.Element element) { if (element.localName == HTMLTag.span) { delta.insert(element.text, From 3f9e16922f61f5f58910812df9654a24483eeb03 Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 15:46:40 +0800 Subject: [PATCH 142/224] feat: paste image directly --- .../packages/flowy_editor/lib/src/infra/html_converter.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart index 0aa8d4fd13..f56184a57c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/infra/html_converter.dart @@ -127,6 +127,8 @@ class HTMLToNodesConverter { return _handleListElement(element); } else if (element.localName == HTMLTag.paragraph) { return [_handleParagraph(element, attributes)]; + } else if (element.localName == HTMLTag.image) { + return [_handleImage(element)]; } else { final delta = Delta(); delta.insert(element.text); From dc8635c6213f5130c533d0a59590e09d0e7d8fbb Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 15:54:43 +0800 Subject: [PATCH 143/224] feat: paste url from plain text --- .../copy_paste_handler.dart | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart index 7c89c2ed83..00826e41f7 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart @@ -149,6 +149,46 @@ _pastRichClipboard(EditorState editorState, RichClipboardData data) { } } +_pasteSingleLine(EditorState editorState, Selection selection, String line) { + final node = editorState.document.nodeAtPath(selection.end.path)! as TextNode; + final beginOffset = selection.end.offset; + TransactionBuilder(editorState) + ..textEdit( + node, + () => Delta() + ..retain(beginOffset) + ..addAll(_lineContentToDelta(line))) + ..setAfterSelection(Selection.collapsed( + Position(path: selection.end.path, offset: beginOffset + line.length))) + ..commit(); +} + +/// parse url from the line text +/// reference: https://stackoverflow.com/questions/59444837/flutter-dart-regex-to-extract-urls-from-a-string +Delta _lineContentToDelta(String lineContent) { + final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'); + final Iterable matches = exp.allMatches(lineContent); + + final delta = Delta(); + + var lastUrlEndOffset = 0; + + for (final match in matches) { + if (lastUrlEndOffset < match.start) { + delta.insert(lineContent.substring(lastUrlEndOffset, match.start)); + } + final linkContent = lineContent.substring(match.start, match.end); + delta.insert(linkContent, {"href": linkContent}); + lastUrlEndOffset = match.end; + } + + if (lastUrlEndOffset < lineContent.length) { + delta.insert(lineContent.substring(lastUrlEndOffset, lineContent.length)); + } + + return delta; +} + _handlePastePlainText(EditorState editorState, String plainText) { final selection = editorState.cursorSelection?.normalize(); if (selection == null) { @@ -163,18 +203,8 @@ _handlePastePlainText(EditorState editorState, String plainText) { if (lines.isEmpty) { return; } else if (lines.length == 1) { - final node = - editorState.document.nodeAtPath(selection.end.path)! as TextNode; - final beginOffset = selection.end.offset; - TransactionBuilder(editorState) - ..textEdit( - node, - () => Delta() - ..retain(beginOffset) - ..insert(lines[0])) - ..setAfterSelection(Selection.collapsed(Position( - path: selection.end.path, offset: beginOffset + lines[0].length))) - ..commit(); + // single line + _pasteSingleLine(editorState, selection, lines.first); } else { final firstLine = lines[0]; final beginOffset = selection.end.offset; @@ -196,11 +226,9 @@ _handlePastePlainText(EditorState editorState, String plainText) { if (index++ == remains.length - 1) { return TextNode( type: "text", - delta: Delta() - ..insert(e) - ..addAll(insertedLineSuffix)); + delta: _lineContentToDelta(e)..addAll(insertedLineSuffix)); } - return TextNode(type: "text", delta: Delta()..insert(e)); + return TextNode(type: "text", delta: _lineContentToDelta(e)); }).toList(); // insert first line tb.textEdit( From e926c89548022957ca677e45efa7fcb72ff2e0f4 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 16:35:22 +0800 Subject: [PATCH 144/224] test: implement simple redo/undo test for no-styled text --- .../src/render/selection/toolbar_widget.dart | 34 ++++------- .../lib/src/service/toolbar_service.dart | 14 ++++- .../flowy_editor/test/infra/test_editor.dart | 16 ++++- .../test/infra/test_raw_key_event.dart | 31 +++++++++- .../redo_undo_handler_test.dart | 60 +++++++++++++++++++ 5 files changed, 127 insertions(+), 28 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart index 68d78f484f..43b3dad432 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart @@ -28,10 +28,12 @@ List defaultListToolbarEventNames = [ 'H1', 'H2', 'H3', - // 'B-List', - // 'N-List', ]; +mixin ToolBarMixin on State { + void hide(); +} + class ToolbarWidget extends StatefulWidget { const ToolbarWidget({ Key? key, @@ -50,7 +52,7 @@ class ToolbarWidget extends StatefulWidget { State createState() => _ToolbarWidgetState(); } -class _ToolbarWidgetState extends State { +class _ToolbarWidgetState extends State with ToolBarMixin { final GlobalKey _listToolbarKey = GlobalKey(); final toolbarHeight = 32.0; @@ -63,21 +65,6 @@ class _ToolbarWidgetState extends State { OverlayEntry? _listToolbarOverlay; - @override - void initState() { - super.initState(); - - widget.editorState.service.selectionService.currentSelection - .addListener(_onSelectionChange); - } - - @override - void dispose() { - widget.editorState.service.selectionService.currentSelection - .removeListener(_onSelectionChange); - super.dispose(); - } - @override Widget build(BuildContext context) { return Positioned( @@ -92,6 +79,12 @@ class _ToolbarWidgetState extends State { ); } + @override + void hide() { + _listToolbarOverlay?.remove(); + _listToolbarOverlay = null; + } + Widget _buildToolbar(BuildContext context) { return Material( borderRadius: BorderRadius.circular(cornerRadius), @@ -212,9 +205,4 @@ class _ToolbarWidgetState extends State { } assert(false, 'Could not find the event handler for $eventName'); } - - void _onSelectionChange() { - _listToolbarOverlay?.remove(); - _listToolbarOverlay = null; - } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart index aaf52bc20c..636893ab8c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/src/render/selection/toolbar_widget.dart'; +import 'package:flowy_editor/src/extensions/object_extensions.dart'; abstract class FlowyToolbarService { /// Show the toolbar widget beside the offset. @@ -28,12 +29,15 @@ class FlowyToolbar extends StatefulWidget { class _FlowyToolbarState extends State implements FlowyToolbarService { OverlayEntry? _toolbarOverlay; + final _toolbarWidgetKey = GlobalKey(debugLabel: '_toolbar_widget'); @override void showInOffset(Offset offset, LayerLink layerLink) { - _toolbarOverlay?.remove(); + hide(); + _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( + key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), @@ -45,6 +49,7 @@ class _FlowyToolbarState extends State @override void hide() { + _toolbarWidgetKey.currentState?.unwrapOrNull()?.hide(); _toolbarOverlay?.remove(); _toolbarOverlay = null; } @@ -55,4 +60,11 @@ class _FlowyToolbarState extends State child: widget.child, ); } + + @override + void dispose() { + hide(); + + super.dispose(); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index ddbe4d5b2c..533cace586 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -72,8 +72,20 @@ class EditorWidgetTester { await tester.pumpAndSettle(); } - Future pressLogicKey(LogicalKeyboardKey key) async { - final testRawKeyEventData = TestRawKeyEventData(logicalKey: key).toKeyEvent; + Future pressLogicKey( + LogicalKeyboardKey key, { + bool isControlPressed = false, + bool isShiftPressed = false, + bool isAltPressed = false, + bool isMetaPressed = false, + }) async { + final testRawKeyEventData = TestRawKeyEventData( + logicalKey: key, + isControlPressed: isControlPressed, + isShiftPressed: isShiftPressed, + isAltPressed: isAltPressed, + isMetaPressed: isMetaPressed, + ).toKeyEvent; _editorState.service.keyboardService!.onKey(testRawKeyEventData); await tester.pumpAndSettle(); } diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index 512ba26574..8fa0e5e4e3 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -1,7 +1,25 @@ import 'package:flutter/services.dart'; class TestRawKeyEvent extends RawKeyDownEvent { - const TestRawKeyEvent({required super.data}); + const TestRawKeyEvent({ + required super.data, + this.isControlPressed = false, + this.isShiftPressed = false, + this.isAltPressed = false, + this.isMetaPressed = false, + }); + + @override + final bool isControlPressed; + + @override + final bool isShiftPressed; + + @override + final bool isAltPressed; + + @override + final bool isMetaPressed; } class TestRawKeyEventData extends RawKeyEventData { @@ -46,7 +64,13 @@ class TestRawKeyEventData extends RawKeyEventData { String get keyLabel => throw UnimplementedError(); RawKeyEvent get toKeyEvent { - return TestRawKeyEvent(data: this); + return TestRawKeyEvent( + data: this, + isAltPressed: isAltPressed, + isControlPressed: isControlPressed, + isMetaPressed: isMetaPressed, + isShiftPressed: isShiftPressed, + ); } } @@ -67,6 +91,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.pageUp) { return PhysicalKeyboardKey.pageUp; } + if (this == LogicalKeyboardKey.keyZ) { + return PhysicalKeyboardKey.keyZ; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart new file mode 100644 index 0000000000..3bd9a81c52 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/redo_undo_handler_test.dart @@ -0,0 +1,60 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('redo_undo_handler_test.dart', () { + // TODO: need to test more cases. + testWidgets('Redo, Undo for backspace key, and selection is downward', + (tester) async { + await _testBackspaceUndoRedo(tester, true); + }); + + testWidgets('Redo, Undo for backspace key, and selection is forward', + (tester) async { + await _testBackspaceUndoRedo(tester, false); + }); + }); +} + +Future _testBackspaceUndoRedo( + WidgetTester tester, bool isDownwardSelection) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + final start = Position(path: [0], offset: text.length); + final end = Position(path: [1], offset: text.length); + final selection = Selection( + start: isDownwardSelection ? start : end, + end: isDownwardSelection ? end : start, + ); + await editor.updateSelection(selection); + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isMetaPressed: true, + ); + + expect(editor.documentLength, 3); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + expect(editor.documentSelection, selection); + + await editor.pressLogicKey( + LogicalKeyboardKey.keyZ, + isMetaPressed: true, + isShiftPressed: true, + ); + + expect(editor.documentLength, 2); +} From c732f4e908fe6f694131256fff1b6b877b28ed2b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 16:48:33 +0800 Subject: [PATCH 145/224] test: implement select all test for no-styled text --- .../test/infra/test_raw_key_event.dart | 3 ++ .../select_all_handler_test.dart | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index 8fa0e5e4e3..78ed06383e 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -94,6 +94,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyZ) { return PhysicalKeyboardKey.keyZ; } + if (this == LogicalKeyboardKey.keyA) { + return PhysicalKeyboardKey.keyA; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart new file mode 100644 index 0000000000..53c13b7119 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/select_all_handler_test.dart @@ -0,0 +1,38 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('select_all_handler_test.dart', () { + testWidgets('Presses Command + A in small document', (tester) async { + await _testSelectAllHandler(tester, 10); + }); + + testWidgets('Presses Command + A in small document', (tester) async { + await _testSelectAllHandler(tester, 1000); + }); + }); +} + +Future _testSelectAllHandler(WidgetTester tester, int lines) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + + expect( + editor.documentSelection, + Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [lines - 1], offset: text.length), + ), + ); +} From f762a78ac025a78bec0f486e39cc2ef7be326d68 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 18:57:59 +0800 Subject: [PATCH 146/224] test: implement update text style by command X test --- .../slash_handler.dart | 30 ++++--- ...pdate_text_style_by_command_x_handler.dart | 36 ++++---- .../test/infra/test_raw_key_event.dart | 19 +++- .../slash_handler_test.dart | 39 +++++++++ ..._text_style_by_command_x_handler_test.dart | 87 +++++++++++++++++++ 5 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart index 83f1b9e13a..093318813d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart @@ -11,6 +11,9 @@ import 'package:flowy_editor/src/extensions/node_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +@visibleForTesting +List get popupListItems => _popupListItems; + final List _popupListItems = [ PopupListItem( text: 'Text', @@ -94,6 +97,14 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { _editorState = editorState; WidgetsBinding.instance.addPostFrameCallback((_) { _selectionChangeBySlash = false; + + editorState.service.selectionService.currentSelection + .removeListener(clearPopupList); + editorState.service.selectionService.currentSelection + .addListener(clearPopupList); + + editorState.service.scrollService?.disable(); + showPopupList(context, editorState, selectionRects.first.bottomRight); }); @@ -115,23 +126,20 @@ void showPopupList( ); Overlay.of(context)?.insert(_popupListOverlay!); - - editorState.service.selectionService.currentSelection - .removeListener(clearPopupList); - editorState.service.selectionService.currentSelection - .addListener(clearPopupList); - - editorState.service.scrollService?.disable(); } void clearPopupList() { if (_popupListOverlay == null || _editorState == null) { return; } - final selection = - _editorState?.service.selectionService.currentSelection.value; - if (selection == null) { - return; + final isSelectionDisposed = + _editorState?.service.selectionServiceKey.currentState != null; + if (isSelectionDisposed) { + final selection = + _editorState?.service.selectionService.currentSelection.value; + if (selection == null) { + return; + } } if (_selectionChangeBySlash) { _selectionChangeBySlash = false; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 4bd9382e3a..4dab96e54a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/src/document/node.dart'; import 'package:flowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; +import 'package:flutter/services.dart'; FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { - if (!event.isMetaPressed || event.character == null) { + if (!event.isMetaPressed) { return KeyEventResult.ignored; } @@ -17,26 +18,19 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { return KeyEventResult.ignored; } - switch (event.character!) { - // bold - case 'B': - case 'b': - formatBold(editorState); - return KeyEventResult.handled; - case 'I': - case 'i': - formatItalic(editorState); - return KeyEventResult.handled; - case 'U': - case 'u': - formatUnderline(editorState); - return KeyEventResult.handled; - case 'S': - case 's': - formatStrikethrough(editorState); - return KeyEventResult.handled; - default: - break; + if (event.logicalKey == LogicalKeyboardKey.keyB) { + formatBold(editorState); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyI) { + formatItalic(editorState); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyU) { + formatUnderline(editorState); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyS && + event.isShiftPressed) { + formatStrikethrough(editorState); + return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index 78ed06383e..d3dac12677 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -91,12 +91,27 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.pageUp) { return PhysicalKeyboardKey.pageUp; } - if (this == LogicalKeyboardKey.keyZ) { - return PhysicalKeyboardKey.keyZ; + if (this == LogicalKeyboardKey.slash) { + return PhysicalKeyboardKey.slash; + } + if (this == LogicalKeyboardKey.arrowDown) { + return PhysicalKeyboardKey.arrowDown; } if (this == LogicalKeyboardKey.keyA) { return PhysicalKeyboardKey.keyA; } + if (this == LogicalKeyboardKey.keyB) { + return PhysicalKeyboardKey.keyB; + } + if (this == LogicalKeyboardKey.keyI) { + return PhysicalKeyboardKey.keyI; + } + if (this == LogicalKeyboardKey.keyS) { + return PhysicalKeyboardKey.keyS; + } + if (this == LogicalKeyboardKey.keyU) { + return PhysicalKeyboardKey.keyU; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart new file mode 100644 index 0000000000..a8d1cc36f1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/slash_handler_test.dart @@ -0,0 +1,39 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('slash_handler.dart', () { + testWidgets('Presses / to trigger popup list ', (tester) async { + const text = 'Welcome to Appflowy 😁'; + const lines = 3; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); + await editor.pressLogicKey(LogicalKeyboardKey.slash); + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(PopupListWidget, skipOffstage: false), findsOneWidget); + + for (final item in popupListItems) { + expect(find.byWidget(item.icon), findsOneWidget); + } + + await editor.updateSelection(Selection.single(path: [1], startOffset: 0)); + + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect(find.byType(PopupListWidget, skipOffstage: false), findsNothing); + }); + }); +} diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart new file mode 100644 index 0000000000..3d4e1aff3c --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -0,0 +1,87 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('update_text_style_by_command_x_handler.dart', () { + testWidgets('Presses Command + B to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.bold, + LogicalKeyboardKey.keyB, + ); + }); + + testWidgets('Presses Command + I to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.bold, + LogicalKeyboardKey.keyI, + ); + }); + + testWidgets('Presses Command + U to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.bold, + LogicalKeyboardKey.keyU, + ); + }); + + testWidgets('Presses Command + S to update text style', (tester) async { + await _testUpdateTextStyleByCommandX( + tester, + StyleKey.bold, + LogicalKeyboardKey.keyS, + ); + }); + }); +} + +Future _testUpdateTextStyleByCommandX( + WidgetTester tester, String matchStyle, LogicalKeyboardKey key) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + var selection = + Selection.single(path: [1], startOffset: 2, endOffset: text.length - 2); + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + var textNode = editor.nodeAtPath([1]) as TextNode; + expect(textNode.allSatisfyInSelection(matchStyle, selection), true); + + selection = + Selection.single(path: [1], startOffset: 0, endOffset: text.length); + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + textNode = editor.nodeAtPath([1]) as TextNode; + expect(textNode.allSatisfyInSelection(matchStyle, selection), true); + + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + textNode = editor.nodeAtPath([1]) as TextNode; + expect(textNode.allSatisfyInSelection(matchStyle, selection), false); +} From ab1032139a4ff1b70fccb14b2ee884859f679047 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 19:01:41 +0800 Subject: [PATCH 147/224] fix: typo --- .../flowy_editor/lib/src/render/selection/toolbar_widget.dart | 4 ++-- .../flowy_editor/lib/src/service/toolbar_service.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart index 43b3dad432..2c9fb9ad93 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart @@ -30,7 +30,7 @@ List defaultListToolbarEventNames = [ 'H3', ]; -mixin ToolBarMixin on State { +mixin ToolbarMixin on State { void hide(); } @@ -52,7 +52,7 @@ class ToolbarWidget extends StatefulWidget { State createState() => _ToolbarWidgetState(); } -class _ToolbarWidgetState extends State with ToolBarMixin { +class _ToolbarWidgetState extends State with ToolbarMixin { final GlobalKey _listToolbarKey = GlobalKey(); final toolbarHeight = 32.0; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart index 636893ab8c..fad7437b63 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/toolbar_service.dart @@ -49,7 +49,7 @@ class _FlowyToolbarState extends State @override void hide() { - _toolbarWidgetKey.currentState?.unwrapOrNull()?.hide(); + _toolbarWidgetKey.currentState?.unwrapOrNull()?.hide(); _toolbarOverlay?.remove(); _toolbarOverlay = null; } From de01e9222ed1ba823c50a138f82f6ef7461a2559 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 19:09:53 +0800 Subject: [PATCH 148/224] test: fix update_test_style_by_command_x_handler_test --- .../flowy_editor/test/infra/test_raw_key_event.dart | 3 +++ .../update_text_style_by_command_x_handler_test.dart | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index d3dac12677..e4eb99b60e 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -112,6 +112,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.keyU) { return PhysicalKeyboardKey.keyU; } + if (this == LogicalKeyboardKey.keyZ) { + return PhysicalKeyboardKey.keyZ; + } throw UnimplementedError(); } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index 3d4e1aff3c..39c750a933 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -22,7 +22,7 @@ void main() async { testWidgets('Presses Command + I to update text style', (tester) async { await _testUpdateTextStyleByCommandX( tester, - StyleKey.bold, + StyleKey.italic, LogicalKeyboardKey.keyI, ); }); @@ -30,7 +30,7 @@ void main() async { testWidgets('Presses Command + U to update text style', (tester) async { await _testUpdateTextStyleByCommandX( tester, - StyleKey.bold, + StyleKey.underline, LogicalKeyboardKey.keyU, ); }); @@ -38,7 +38,7 @@ void main() async { testWidgets('Presses Command + S to update text style', (tester) async { await _testUpdateTextStyleByCommandX( tester, - StyleKey.bold, + StyleKey.strikethrough, LogicalKeyboardKey.keyS, ); }); From 0514b005cab4d4bf66a7d4efcb74fd589f88de4f Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 15 Aug 2022 20:07:01 +0800 Subject: [PATCH 149/224] feat: config view lens --- .../app_flowy/lib/plugins/board/board.dart | 2 +- frontend/rust-lib/Cargo.lock | 1 + .../2022-08-15-020544_grid-view/down.sql | 2 +- .../2022-08-15-020544_grid-view/up.sql | 2 +- .../rust-lib/flowy-database/src/macros.rs | 14 +- .../rust-lib/flowy-database/src/schema.rs | 4 +- .../src/services/folder_editor.rs | 5 +- frontend/rust-lib/flowy-grid/Cargo.toml | 1 + .../src/entities/group_entities/board_card.rs | 15 +- .../flowy-grid/src/entities/row_entities.rs | 2 + .../rust-lib/flowy-grid/src/event_handler.rs | 6 +- frontend/rust-lib/flowy-grid/src/manager.rs | 2 +- ...ock_revision_editor.rs => block_editor.rs} | 6 +- .../flowy-grid/src/services/block_manager.rs | 26 +- .../src/services/filter/filter_cache.rs | 11 +- .../src/services/filter/filter_service.rs | 7 + .../flowy-grid/src/services/grid_editor.rs | 115 +++------ .../src/services/grid_view_editor.rs | 163 +++++++++++- .../src/services/grid_view_manager.rs | 204 +++++++++++++++ .../group/group_generator/checkbox_group.rs | 5 +- .../group/group_generator/generator.rs | 17 +- .../group_generator/select_option_group.rs | 6 +- .../src/services/group/group_service.rs | 112 ++++----- .../rust-lib/flowy-grid/src/services/mod.rs | 3 +- .../tests/grid/block_test/script.rs | 9 +- .../src/cache/disk/document_impl.rs | 2 +- .../src/cache/disk/grid_block_impl.rs | 26 +- .../src/cache/disk/grid_impl.rs | 3 +- .../src/cache/disk/grid_view_impl.rs | 233 ++++++++++++++++++ .../flowy-revision/src/cache/disk/mod.rs | 2 + .../src/revision/grid_rev.rs | 7 +- .../src/revision/grid_view.rs | 3 +- .../flowy-sync/src/client_folder/builder.rs | 6 +- .../src/client_grid/grid_revision_pad.rs | 120 +-------- .../src/client_grid/view_revision_pad.rs | 77 +++++- shared-lib/lib-infra/src/future.rs | 8 +- 36 files changed, 853 insertions(+), 374 deletions(-) rename frontend/rust-lib/flowy-grid/src/services/{block_revision_editor.rs => block_editor.rs} (97%) create mode 100644 frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs create mode 100644 frontend/rust-lib/flowy-revision/src/cache/disk/grid_view_impl.rs diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 2954a7cbf9..36d181ae3e 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class BoardPlugin extends Plugin { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 868e04a211..fba225a3a3 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -935,6 +935,7 @@ dependencies = [ "atomic_refcell", "bytes", "chrono", + "crossbeam-utils", "dart-notify", "dashmap", "diesel", diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql index c2b23b0180..aff09f3bb9 100644 --- a/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -DROP TABLE grid_view_table; \ No newline at end of file +DROP TABLE grid_view_rev_table; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql index d6e526aba4..e2b39801e1 100644 --- a/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql +++ b/frontend/rust-lib/flowy-database/migrations/2022-08-15-020544_grid-view/up.sql @@ -1,7 +1,7 @@ -- Your SQL goes here -CREATE TABLE grid_view_table ( +CREATE TABLE grid_view_rev_table ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, object_id TEXT NOT NULL DEFAULT '', base_rev_id BIGINT NOT NULL DEFAULT 0, diff --git a/frontend/rust-lib/flowy-database/src/macros.rs b/frontend/rust-lib/flowy-database/src/macros.rs index e1534bf25f..870938c1d0 100644 --- a/frontend/rust-lib/flowy-database/src/macros.rs +++ b/frontend/rust-lib/flowy-database/src/macros.rs @@ -177,20 +177,20 @@ macro_rules! impl_rev_state_map { } } - impl std::convert::From<$target> for RevisionState { + impl std::convert::From<$target> for crate::disk::RevisionState { fn from(s: $target) -> Self { match s { - $target::Sync => RevisionState::Sync, - $target::Ack => RevisionState::Ack, + $target::Sync => crate::disk::RevisionState::Sync, + $target::Ack => crate::disk::RevisionState::Ack, } } } - impl std::convert::From for $target { - fn from(s: RevisionState) -> Self { + impl std::convert::From for $target { + fn from(s: crate::disk::RevisionState) -> Self { match s { - RevisionState::Sync => $target::Sync, - RevisionState::Ack => $target::Ack, + crate::disk::RevisionState::Sync => $target::Sync, + crate::disk::RevisionState::Ack => $target::Ack, } } } diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index ae616622b7..065a13b85f 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -43,7 +43,7 @@ table! { } table! { - grid_view_table (id) { + grid_view_rev_table (id) { id -> Integer, object_id -> Text, base_rev_id -> BigInt, @@ -136,7 +136,7 @@ allow_tables_to_appear_in_same_query!( grid_block_index_table, grid_meta_rev_table, grid_rev_table, - grid_view_table, + grid_view_rev_table, kv_table, rev_snapshot, rev_table, diff --git a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs index af0ae132f1..6f68497edd 100644 --- a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs +++ b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs @@ -4,13 +4,12 @@ use flowy_error::{FlowyError, FlowyResult}; use flowy_revision::{ RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder, RevisionWebSocket, }; -use flowy_sync::util::make_delta_from_revisions; +use flowy_sync::util::make_text_delta_from_revisions; use flowy_sync::{ client_folder::{FolderChangeset, FolderPad}, entities::{revision::Revision, ws_data::ServerRevisionWSData}, }; use lib_infra::future::FutureResult; -use lib_ot::core::PhantomAttributes; use parking_lot::RwLock; use std::sync::Arc; @@ -132,7 +131,7 @@ impl FolderEditor { pub struct FolderRevisionCompactor(); impl RevisionCompactor for FolderRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; + let delta = make_text_delta_from_revisions(revisions)?; Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/Cargo.toml b/frontend/rust-lib/flowy-grid/Cargo.toml index ba3702038e..709359560b 100644 --- a/frontend/rust-lib/flowy-grid/Cargo.toml +++ b/frontend/rust-lib/flowy-grid/Cargo.toml @@ -40,6 +40,7 @@ regex = "1.5.6" url = { version = "2"} futures = "0.3.15" atomic_refcell = "0.1.8" +crossbeam-utils = "0.8.7" [dev-dependencies] flowy-test = { path = "../flowy-test" } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs index e2dac9069d..1ce2b40358 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs @@ -1,4 +1,4 @@ -use crate::entities::RowPB; +use crate::entities::{CreateRowParams, RowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -11,20 +11,17 @@ pub struct CreateBoardCardPayloadPB { #[pb(index = 2)] pub group_id: String, } -pub struct CreateBoardCardParams { - pub grid_id: String, - pub group_id: String, -} -impl TryInto for CreateBoardCardPayloadPB { +impl TryInto for CreateBoardCardPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let group_id = NotEmptyStr::parse(self.group_id).map_err(|_| ErrorCode::GroupIdIsEmpty)?; - Ok(CreateBoardCardParams { + Ok(CreateRowParams { grid_id: grid_id.0, - group_id: group_id.0, + start_row_id: None, + group_id: Some(group_id.0), }) } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs index 47af2e00d3..d42052c747 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs @@ -58,6 +58,7 @@ pub struct CreateRowPayloadPB { pub struct CreateRowParams { pub grid_id: String, pub start_row_id: Option, + pub group_id: Option, } impl TryInto for CreateRowPayloadPB { @@ -68,6 +69,7 @@ impl TryInto for CreateRowPayloadPB { Ok(CreateRowParams { grid_id: grid_id.0, start_row_id: self.start_row_id, + group_id: None, }) } } diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 2c119c55dc..3001ecb52d 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -266,7 +266,7 @@ pub(crate) async fn create_row_handler( ) -> DataResult { let params: CreateRowParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(params.grid_id.as_ref())?; - let row = editor.create_row(params.start_row_id).await?; + let row = editor.create_row(params).await?; data_result(row) } @@ -419,8 +419,8 @@ pub(crate) async fn create_board_card_handler( data: Data, manager: AppData>, ) -> DataResult { - let params: CreateBoardCardParams = data.into_inner().try_into()?; + let params: CreateRowParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(params.grid_id.as_ref())?; - let row = editor.create_board_card(¶ms.group_id).await?; + let row = editor.create_row(params).await?; data_result(row) } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 19a3669c46..f99e6ca4c6 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -1,4 +1,4 @@ -use crate::services::block_revision_editor::GridBlockRevisionCompactor; +use crate::services::block_editor::GridBlockRevisionCompactor; use crate::services::grid_editor::{GridRevisionCompactor, GridRevisionEditor}; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::persistence::kv::GridKVPersistence; diff --git a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_editor.rs similarity index 97% rename from frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs rename to frontend/rust-lib/flowy-grid/src/services/block_editor.rs index a674098bfc..a53866ee48 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_editor.rs @@ -5,9 +5,9 @@ use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowMetaCh use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder}; use flowy_sync::client_grid::{GridBlockRevisionChangeset, GridBlockRevisionPad}; use flowy_sync::entities::revision::Revision; -use flowy_sync::util::make_delta_from_revisions; +use flowy_sync::util::make_text_delta_from_revisions; use lib_infra::future::FutureResult; -use lib_ot::core::PhantomAttributes; + use std::borrow::Cow; use std::sync::Arc; use tokio::sync::RwLock; @@ -200,7 +200,7 @@ impl RevisionObjectBuilder for GridBlockRevisionPadBuilder { pub struct GridBlockRevisionCompactor(); impl RevisionCompactor for GridBlockRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; + let delta = make_text_delta_from_revisions(revisions)?; Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index b2b6ce63fc..457215f8be 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -1,7 +1,7 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{CellChangesetPB, GridBlockChangesetPB, InsertedRowPB, RowPB}; use crate::manager::GridUser; -use crate::services::block_revision_editor::{GridBlockRevisionCompactor, GridBlockRevisionEditor}; +use crate::services::block_editor::{GridBlockRevisionCompactor, GridBlockRevisionEditor}; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{block_from_row_orders, make_row_from_row_rev, GridBlockSnapshot}; use dashmap::DashMap; @@ -17,8 +17,6 @@ use std::sync::Arc; type BlockId = String; pub(crate) struct GridBlockManager { - #[allow(dead_code)] - grid_id: String, user: Arc, persistence: Arc, block_editors: DashMap>, @@ -26,16 +24,13 @@ pub(crate) struct GridBlockManager { impl GridBlockManager { pub(crate) async fn new( - grid_id: &str, user: &Arc, block_meta_revs: Vec>, persistence: Arc, ) -> FlowyResult { let block_editors = make_block_editors(user, block_meta_revs).await?; let user = user.clone(); - let grid_id = grid_id.to_owned(); let manager = Self { - grid_id, user, block_editors, persistence, @@ -44,7 +39,7 @@ impl GridBlockManager { } // #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn get_editor(&self, block_id: &str) -> FlowyResult> { + pub(crate) async fn get_block_editor(&self, block_id: &str) -> FlowyResult> { debug_assert!(!block_id.is_empty()); match self.block_editors.get(block_id) { None => { @@ -59,13 +54,13 @@ impl GridBlockManager { async fn get_editor_from_row_id(&self, row_id: &str) -> FlowyResult> { let block_id = self.persistence.get_block_id(row_id)?; - Ok(self.get_editor(&block_id).await?) + Ok(self.get_block_editor(&block_id).await?) } pub(crate) async fn create_row(&self, row_rev: RowRevision, start_row_id: Option) -> FlowyResult { let block_id = row_rev.block_id.clone(); let _ = self.persistence.insert(&row_rev.block_id, &row_rev.id)?; - let editor = self.get_editor(&row_rev.block_id).await?; + let editor = self.get_block_editor(&row_rev.block_id).await?; let mut index_row_order = InsertedRowPB::from(&row_rev); let (row_count, row_index) = editor.create_row(row_rev, start_row_id).await?; @@ -82,7 +77,7 @@ impl GridBlockManager { let mut changesets = vec![]; for (block_id, row_revs) in rows_by_block_id { let mut inserted_row_orders = vec![]; - let editor = self.get_editor(&block_id).await?; + let editor = self.get_block_editor(&block_id).await?; let mut row_count = 0; for row in row_revs { let _ = self.persistence.insert(&row.block_id, &row.id)?; @@ -130,7 +125,7 @@ impl GridBlockManager { pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let row_id = row_id.to_owned(); let block_id = self.persistence.get_block_id(&row_id)?; - let editor = self.get_editor(&block_id).await?; + let editor = self.get_block_editor(&block_id).await?; match editor.get_row_info(&row_id).await? { None => {} Some(row_info) => { @@ -147,7 +142,7 @@ impl GridBlockManager { pub(crate) async fn delete_rows(&self, row_orders: Vec) -> FlowyResult> { let mut changesets = vec![]; for grid_block in block_from_row_orders(row_orders) { - let editor = self.get_editor(&grid_block.id).await?; + let editor = self.get_block_editor(&grid_block.id).await?; let row_ids = grid_block .rows .into_iter() @@ -207,7 +202,7 @@ impl GridBlockManager { } pub async fn get_row_orders(&self, block_id: &str) -> FlowyResult> { - let editor = self.get_editor(block_id).await?; + let editor = self.get_block_editor(block_id).await?; editor.get_row_infos::<&str>(None).await } @@ -227,7 +222,7 @@ impl GridBlockManager { } Some(block_ids) => { for block_id in block_ids { - let editor = self.get_editor(&block_id).await?; + let editor = self.get_block_editor(&block_id).await?; let row_revs = editor.get_row_revs::<&str>(None).await?; snapshots.push(GridBlockSnapshot { block_id, row_revs }); } @@ -250,6 +245,7 @@ impl GridBlockManager { } } +/// Initialize each block editor async fn make_block_editors( user: &Arc, block_meta_revs: Vec>, @@ -264,7 +260,7 @@ async fn make_block_editors( } async fn make_block_editor(user: &Arc, block_id: &str) -> FlowyResult { - tracing::trace!("Open block:{} meta editor", block_id); + tracing::trace!("Open block:{} editor", block_id); let token = user.token()?; let user_id = user.user_id()?; let pool = user.db_pool()?; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs index 85a53c4d6f..903779c0e8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_cache.rs @@ -3,7 +3,7 @@ use crate::entities::{ SelectOptionFilterConfigurationPB, TextFilterConfigurationPB, }; use dashmap::DashMap; -use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; +use flowy_grid_data_model::revision::{FieldRevision, FilterConfigurationRevision, RowRevision}; use flowy_sync::client_grid::GridRevisionPad; use std::collections::HashMap; use std::sync::Arc; @@ -74,6 +74,7 @@ impl FilterCache { this } + #[allow(dead_code)] pub(crate) fn remove(&self, filter_id: &FilterId) { let _ = match filter_id.field_type { FieldType::RichText => { @@ -104,13 +105,15 @@ impl FilterCache { /// Refresh the filter according to the field id. pub(crate) async fn refresh_filter_cache( cache: Arc, - field_ids: Option>, + _field_ids: Option>, grid_pad: &Arc>, ) { let grid_pad = grid_pad.read().await; - let filters_revs = grid_pad.get_filters(field_ids).unwrap_or_default(); + // let filters_revs = grid_pad.get_filters(field_ids).unwrap_or_default(); + // TODO nathan + let filter_revs: Vec> = vec![]; - for filter_rev in filters_revs { + for filter_rev in filter_revs { match grid_pad.get_field_rev(&filter_rev.field_id) { None => {} Some((_, field_rev)) => { diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs index c9850d1c6c..409c5bc5ba 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs @@ -1,3 +1,8 @@ +#![allow(clippy::all)] +#![allow(unused_attributes)] +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_results)] use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{FieldType, GridBlockChangesetPB}; use crate::services::block_manager::GridBlockManager; @@ -22,8 +27,10 @@ use std::sync::Arc; use tokio::sync::RwLock; pub(crate) struct GridFilterService { + #[allow(dead_code)] scheduler: Arc, grid_pad: Arc>, + #[allow(dead_code)] block_manager: Arc, filter_cache: Arc, filter_result_cache: Arc, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 812ce561c1..cb35819b55 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -5,15 +5,13 @@ use crate::manager::{GridTaskSchedulerRwLock, GridUser}; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, CellBytes}; use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder}; -use crate::services::filter::{GridFilterChangeset, GridFilterService}; - -use crate::services::grid_view_editor::GridViewRevisionEditor; -use crate::services::group::GridGroupService; +use crate::services::filter::GridFilterService; +use crate::services::grid_view_manager::GridViewManager; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{ make_grid_blocks, make_row_from_row_rev, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder, }; -use crate::services::setting::make_grid_setting; + use bytes::Bytes; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::*; @@ -22,26 +20,23 @@ use flowy_sync::client_grid::{GridRevisionChangeset, GridRevisionPad, JsonDeseri use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; use flowy_sync::entities::revision::Revision; use flowy_sync::errors::CollaborateResult; -use flowy_sync::util::make_delta_from_revisions; +use flowy_sync::util::make_text_delta_from_revisions; use lib_infra::future::FutureResult; -use lib_ot::core::PhantomAttributes; + use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; pub struct GridRevisionEditor { - pub(crate) grid_id: String, + pub grid_id: String, user: Arc, grid_pad: Arc>, - view_editor: Arc, + view_manager: Arc, rev_manager: Arc, block_manager: Arc, #[allow(dead_code)] pub(crate) filter_service: Arc, - - #[allow(dead_code)] - pub(crate) group_service: Arc>, } impl Drop for GridRevisionEditor { @@ -64,20 +59,30 @@ impl GridRevisionEditor { let rev_manager = Arc::new(rev_manager); let grid_pad = Arc::new(RwLock::new(grid_pad)); + // Block manager let block_meta_revs = grid_pad.read().await.get_block_meta_revs(); - let block_manager = Arc::new(GridBlockManager::new(grid_id, &user, block_meta_revs, persistence).await?); + let block_manager = Arc::new(GridBlockManager::new(&user, block_meta_revs, persistence).await?); let filter_service = GridFilterService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await; - let group_service = - GridGroupService::new(grid_pad.clone(), block_manager.clone(), task_scheduler.clone()).await; + + // View manager + let view_manager = Arc::new( + GridViewManager::new( + user.clone(), + grid_pad.clone(), + block_manager.clone(), + Arc::new(task_scheduler.clone()), + ) + .await?, + ); let editor = Arc::new(Self { grid_id: grid_id.to_owned(), user, grid_pad, rev_manager, block_manager, + view_manager, filter_service: Arc::new(filter_service), - group_service: Arc::new(RwLock::new(group_service)), }); Ok(editor) @@ -279,9 +284,15 @@ impl GridRevisionEditor { Ok(()) } - pub async fn create_row(&self, start_row_id: Option) -> FlowyResult { - let row_rev = self.create_row_rev().await?; - self.create_row_pb(row_rev, start_row_id).await + pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult { + let mut row_rev = self.create_row_rev().await?; + + self.view_manager.update_row(&mut row_rev, ¶ms).await; + + let row_pb = self.create_row_pb(row_rev, params.start_row_id.clone()).await?; + + self.view_manager.did_create_row(&row_pb, ¶ms).await; + Ok(row_pb) } pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { @@ -331,7 +342,7 @@ impl GridRevisionEditor { pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let _ = self.block_manager.delete_row(row_id).await?; - self.group_service.read().await.did_delete_card(row_id.to_owned()).await; + self.view_manager.delete_row(row_id).await; Ok(()) } @@ -445,34 +456,15 @@ impl GridRevisionEditor { } pub async fn get_grid_setting(&self) -> FlowyResult { - let read_guard = self.grid_pad.read().await; - let grid_setting_rev = read_guard.get_setting_rev(); - let field_revs = read_guard.get_field_revs(None)?; - let grid_setting = make_grid_setting(grid_setting_rev, &field_revs); - Ok(grid_setting) + self.view_manager.get_setting().await } pub async fn get_grid_filter(&self) -> FlowyResult> { - let read_guard = self.grid_pad.read().await; - match read_guard.get_filters(None) { - Some(filter_revs) => Ok(filter_revs - .iter() - .map(|filter_rev| filter_rev.as_ref().into()) - .collect::>()), - None => Ok(vec![]), - } + self.view_manager.get_filters().await } pub async fn update_grid_setting(&self, params: GridSettingChangesetParams) -> FlowyResult<()> { - let filter_changeset = GridFilterChangeset::from(¶ms); - let _ = self - .modify(|grid_pad| Ok(grid_pad.update_grid_setting_rev(params)?)) - .await?; - - let filter_service = self.filter_service.clone(); - tokio::spawn(async move { - filter_service.apply_changeset(filter_changeset).await; - }); + let _ = self.view_manager.update_setting(params).await?; Ok(()) } @@ -522,22 +514,9 @@ impl GridRevisionEditor { } pub async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> { - match self.block_manager.get_row_rev(row_id).await? { - None => tracing::warn!("Move row failed, can not find the row:{}", row_id), - Some(row_rev) => { - let _ = self - .block_manager - .move_row(row_rev.clone(), from as usize, to as usize) - .await?; - } - } - Ok(()) + self.view_manager.move_row(row_id, from, to).await } - pub async fn move_board_card(&self, group_id: &str, from: i32, to: i32) -> FlowyResult<()> { - self.group_service.write().await.move_card(group_id, from, to).await; - Ok(()) - } pub async fn delta_bytes(&self) -> Bytes { self.grid_pad.read().await.delta_bytes() } @@ -550,7 +529,10 @@ impl GridRevisionEditor { let mut blocks_meta_data = vec![]; if original_blocks.len() == duplicated_blocks.len() { for (index, original_block_meta) in original_blocks.iter().enumerate() { - let grid_block_meta_editor = self.block_manager.get_editor(&original_block_meta.block_id).await?; + let grid_block_meta_editor = self + .block_manager + .get_block_editor(&original_block_meta.block_id) + .await?; let duplicated_block_id = &duplicated_blocks[index].block_id; tracing::trace!("Duplicate block:{} meta data", duplicated_block_id); @@ -569,24 +551,9 @@ impl GridRevisionEditor { }) } - pub async fn create_board_card(&self, group_id: &str) -> FlowyResult { - let mut row_rev = self.create_row_rev().await?; - let _ = self - .group_service - .write() - .await - .update_board_card(&mut row_rev, group_id) - .await; - - let row_pb = self.create_row_pb(row_rev, None).await?; - self.group_service.read().await.did_create_card(group_id, &row_pb).await; - Ok(row_pb) - } - #[tracing::instrument(level = "trace", skip_all, err)] pub async fn load_groups(&self) -> FlowyResult { - let groups = self.group_service.write().await.load_groups().await.unwrap_or_default(); - Ok(RepeatedGridGroupPB { items: groups }) + self.view_manager.load_groups().await } async fn create_row_rev(&self) -> FlowyResult { @@ -717,7 +684,7 @@ impl RevisionCloudService for GridRevisionCloudService { pub struct GridRevisionCompactor(); impl RevisionCompactor for GridRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; + let delta = make_text_delta_from_revisions(revisions)?; Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index e37ca9896a..937d7d725b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,31 +1,163 @@ use flowy_error::{FlowyError, FlowyResult}; +use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, GroupPB, RowPB}; +use crate::services::grid_editor_task::GridServiceTaskScheduler; +use crate::services::group::{default_group_configuration, GroupConfigurationDelegate, GroupService}; +use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowRevision}; use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder}; -use flowy_sync::client_grid::GridViewRevisionPad; +use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; use flowy_sync::entities::revision::Revision; -use lib_infra::future::FutureResult; + +use crate::services::setting::make_grid_setting; +use flowy_sync::entities::grid::GridSettingChangesetParams; +use lib_infra::future::{wrap_future, AFFuture, FutureResult}; use std::sync::Arc; use tokio::sync::RwLock; +pub trait GridViewRevisionDelegate: Send + Sync + 'static { + fn get_field_revs(&self) -> AFFuture>>; + fn get_field_rev(&self, field_id: &str) -> AFFuture>>; +} + +pub trait GridViewRevisionDataSource: Send + Sync + 'static { + fn row_revs(&self) -> AFFuture>>; +} + #[allow(dead_code)] pub struct GridViewRevisionEditor { - #[allow(dead_code)] + user_id: String, + view_id: String, pad: Arc>, - #[allow(dead_code)] rev_manager: Arc, + delegate: Arc, + data_source: Arc, + group_service: Arc>, + scheduler: Arc, } impl GridViewRevisionEditor { - #[allow(dead_code)] - pub async fn new(token: &str, mut rev_manager: RevisionManager) -> FlowyResult { + pub(crate) async fn new( + user_id: &str, + token: &str, + view_id: String, + delegate: Delegate, + data_source: DataSource, + scheduler: Arc, + mut rev_manager: RevisionManager, + ) -> FlowyResult + where + Delegate: GridViewRevisionDelegate, + DataSource: GridViewRevisionDataSource, + { let cloud = Arc::new(GridViewRevisionCloudService { token: token.to_owned(), }); let view_revision_pad = rev_manager.load::(Some(cloud)).await?; let pad = Arc::new(RwLock::new(view_revision_pad)); let rev_manager = Arc::new(rev_manager); + let group_service = GroupService::new(Box::new(pad.clone())).await; + let user_id = user_id.to_owned(); + Ok(Self { + pad, + user_id, + view_id, + rev_manager, + scheduler, + delegate: Arc::new(delegate), + data_source: Arc::new(data_source), + group_service: Arc::new(RwLock::new(group_service)), + }) + } - Ok(Self { pad, rev_manager }) + pub(crate) async fn create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { + match params.group_id.as_ref() { + None => {} + Some(group_id) => { + self.group_service + .read() + .await + .update_row(row_rev, group_id, |field_id| self.delegate.get_field_rev(&field_id)) + .await; + } + } + todo!() + } + + pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { + match params.group_id.as_ref() { + None => {} + Some(group_id) => { + self.group_service.read().await.did_create_row(group_id, row_pb).await; + } + } + } + + pub(crate) async fn delete_row(&self, row_id: &str) { + self.group_service.read().await.did_delete_card(row_id.to_owned()).await; + } + + pub(crate) async fn load_groups(&self) -> FlowyResult> { + let field_revs = self.delegate.get_field_revs().await; + let row_revs = self.data_source.row_revs().await; + let mut write_guard = self.group_service.write().await; + match write_guard.load_groups(&field_revs, row_revs).await { + None => Ok(vec![]), + Some(groups) => Ok(groups), + } + } + + pub(crate) async fn get_setting(&self) -> GridSettingPB { + let field_revs = self.delegate.get_field_revs().await; + let grid_setting = make_grid_setting(self.pad.read().await.get_setting_rev(), &field_revs); + grid_setting + } + + pub(crate) async fn update_setting(&self, changeset: GridSettingChangesetParams) -> FlowyResult<()> { + let _ = self.modify(|pad| Ok(pad.update_setting(changeset)?)).await; + Ok(()) + } + + pub(crate) async fn get_filters(&self) -> Vec { + let field_revs = self.delegate.get_field_revs().await; + match self.pad.read().await.get_setting_rev().get_all_filters(&field_revs) { + None => vec![], + Some(filters) => filters + .into_values() + .flatten() + .map(|filter| GridFilterConfiguration::from(filter.as_ref())) + .collect(), + } + } + + async fn modify(&self, f: F) -> FlowyResult<()> + where + F: for<'a> FnOnce(&'a mut GridViewRevisionPad) -> FlowyResult>, + { + let mut write_guard = self.pad.write().await; + match f(&mut *write_guard)? { + None => {} + Some(change) => { + let _ = self.apply_change(change).await?; + } + } + Ok(()) + } + + async fn apply_change(&self, change: GridViewRevisionChangeset) -> FlowyResult<()> { + let GridViewRevisionChangeset { delta, md5 } = change; + let user_id = self.user_id.clone(); + let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); + let delta_data = delta.json_bytes(); + let revision = Revision::new( + &self.rev_manager.object_id, + base_rev_id, + rev_id, + delta_data, + &user_id, + md5, + ); + let _ = self.rev_manager.add_local_revision(&revision).await?; + Ok(()) } } @@ -50,3 +182,20 @@ impl RevisionObjectBuilder for GridViewRevisionPadBuilder { Ok(pad) } } + +impl GroupConfigurationDelegate for Arc> { + fn get_group_configuration(&self, field_rev: Arc) -> AFFuture { + let view_pad = self.clone(); + wrap_future(async move { + let grid_pad = view_pad.read().await; + let configurations = grid_pad.get_groups(&field_rev.id, &field_rev.field_type_rev); + match configurations { + None => default_group_configuration(&field_rev), + Some(mut configurations) => { + assert_eq!(configurations.len(), 1); + (&*configurations.pop().unwrap()).clone() + } + } + }) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs new file mode 100644 index 0000000000..4daf2af667 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -0,0 +1,204 @@ +use crate::manager::GridUser; +use crate::services::grid_view_editor::{GridViewRevisionDataSource, GridViewRevisionDelegate, GridViewRevisionEditor}; +use bytes::Bytes; + +use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB}; +use crate::services::grid_editor_task::GridServiceTaskScheduler; + +use crate::services::block_manager::GridBlockManager; +use dashmap::DashMap; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; +use flowy_revision::disk::SQLiteGridViewRevisionPersistence; +use flowy_revision::{RevisionCompactor, RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence}; +use flowy_sync::client_grid::GridRevisionPad; +use flowy_sync::entities::revision::Revision; + +use flowy_sync::util::make_text_delta_from_revisions; + +use flowy_sync::entities::grid::GridSettingChangesetParams; + +use lib_infra::future::{wrap_future, AFFuture}; +use std::sync::Arc; +use tokio::sync::RwLock; + +type ViewId = String; + +pub(crate) struct GridViewManager { + user: Arc, + grid_pad: Arc>, + block_manager: Arc, + view_editors: DashMap>, + scheduler: Arc, +} + +impl GridViewManager { + pub(crate) async fn new( + user: Arc, + grid_pad: Arc>, + block_manager: Arc, + scheduler: Arc, + ) -> FlowyResult { + Ok(Self { + user, + grid_pad, + scheduler, + block_manager, + view_editors: DashMap::default(), + }) + } + + pub(crate) async fn update_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { + for view_editor in self.view_editors.iter() { + view_editor.create_row(row_rev, params).await; + } + } + + pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { + for view_editor in self.view_editors.iter() { + view_editor.did_create_row(row_pb, params).await; + } + } + + pub(crate) async fn delete_row(&self, row_id: &str) { + for view_editor in self.view_editors.iter() { + view_editor.delete_row(row_id).await; + } + } + + pub(crate) async fn get_setting(&self) -> FlowyResult { + let view_editor = self.get_default_view_editor().await?; + Ok(view_editor.get_setting().await) + } + + pub(crate) async fn update_setting(&self, params: GridSettingChangesetParams) -> FlowyResult<()> { + let view_editor = self.get_default_view_editor().await?; + let _ = view_editor.update_setting(params).await?; + Ok(()) + } + + pub(crate) async fn get_filters(&self) -> FlowyResult> { + let view_editor = self.get_default_view_editor().await?; + Ok(view_editor.get_filters().await) + } + + pub(crate) async fn load_groups(&self) -> FlowyResult { + let view_editor = self.get_default_view_editor().await?; + let groups = view_editor.load_groups().await?; + Ok(RepeatedGridGroupPB { items: groups }) + } + + pub(crate) async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> { + match self.block_manager.get_row_rev(row_id).await? { + None => tracing::warn!("Move row failed, can not find the row:{}", row_id), + Some(row_rev) => { + let _ = self + .block_manager + .move_row(row_rev.clone(), from as usize, to as usize) + .await?; + } + } + Ok(()) + } + + pub(crate) async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { + debug_assert!(!view_id.is_empty()); + match self.view_editors.get(view_id) { + None => { + let editor = Arc::new( + make_view_editor( + &self.user, + view_id, + self.grid_pad.clone(), + self.block_manager.clone(), + self.scheduler.clone(), + ) + .await?, + ); + self.view_editors.insert(view_id.to_owned(), editor.clone()); + Ok(editor) + } + Some(view_editor) => Ok(view_editor.clone()), + } + } + + async fn get_default_view_editor(&self) -> FlowyResult> { + let grid_id = self.grid_pad.read().await.grid_id(); + self.get_view_editor(&grid_id).await + } +} + +async fn make_view_editor( + user: &Arc, + view_id: &str, + delegate: Delegate, + data_source: DataSource, + scheduler: Arc, +) -> FlowyResult +where + Delegate: GridViewRevisionDelegate, + DataSource: GridViewRevisionDataSource, +{ + tracing::trace!("Open view:{} editor", view_id); + let token = user.token()?; + let user_id = user.user_id()?; + let pool = user.db_pool()?; + let view_id = view_id.to_owned(); + + let disk_cache = SQLiteGridViewRevisionPersistence::new(&user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(&user_id, &view_id, disk_cache); + let rev_compactor = GridViewRevisionCompactor(); + + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(&view_id, pool); + let rev_manager = RevisionManager::new(&user_id, &view_id, rev_persistence, rev_compactor, snapshot_persistence); + GridViewRevisionEditor::new(&user_id, &token, view_id, delegate, data_source, scheduler, rev_manager).await +} + +pub struct GridViewRevisionCompactor(); +impl RevisionCompactor for GridViewRevisionCompactor { + fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { + let delta = make_text_delta_from_revisions(revisions)?; + Ok(delta.json_bytes()) + } +} + +impl GridViewRevisionDataSource for Arc { + fn row_revs(&self) -> AFFuture>> { + let block_manager = self.clone(); + + wrap_future(async move { + let blocks = block_manager.get_block_snapshots(None).await.unwrap(); + blocks + .into_iter() + .map(|block| block.row_revs) + .flatten() + .collect::>>() + }) + } +} + +impl GridViewRevisionDelegate for Arc> { + fn get_field_revs(&self) -> AFFuture>> { + let pad = self.clone(); + wrap_future(async move { + match pad.read().await.get_field_revs(None) { + Ok(field_revs) => field_revs, + Err(e) => { + tracing::error!("[GridViewRevisionDelegate] get field revisions failed: {}", e); + vec![] + } + } + }) + } + + fn get_field_rev(&self, field_id: &str) -> AFFuture>> { + let pad = self.clone(); + let field_id = field_id.to_owned(); + wrap_future(async move { + pad.read() + .await + .get_field_rev(&field_id) + .map(|(_, field_rev)| field_rev.clone()) + }) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs index 85c135d0ee..6ea0c78be2 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -4,9 +4,7 @@ use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; use std::sync::Arc; use crate::services::field::{CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOptionPB, CHECK, UNCHECK}; -use crate::services::group::{ - Group, GroupActionHandler, GroupCellContentProvider, GroupController, GroupGenerator, Groupable, -}; +use crate::services::group::{Group, GroupActionHandler, GroupController, GroupGenerator, Groupable}; pub type CheckboxGroupController = GroupController; @@ -45,7 +43,6 @@ impl GroupGenerator for CheckboxGroupGenerator { fn generate_groups( _configuration: &Option, _type_option: &Option, - _cell_content_provider: &dyn GroupCellContentProvider, ) -> Vec { let check_group = Group { id: "true".to_string(), diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index a351913546..0f15538acb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -11,14 +11,6 @@ use indexmap::IndexMap; use std::marker::PhantomData; use std::sync::Arc; -pub trait GroupCellContentProvider { - /// We need to group the rows base on the deduplication cell content when the field type is - /// RichText. - fn deduplication_cell_content(&self, _field_id: &str) -> Vec { - vec![] - } -} - pub trait GroupGenerator { type ConfigurationType; type TypeOptionType; @@ -26,7 +18,6 @@ pub trait GroupGenerator { fn generate_groups( configuration: &Option, type_option: &Option, - cell_content_provider: &dyn GroupCellContentProvider, ) -> Vec; } @@ -86,18 +77,14 @@ where T: TypeOptionDataDeserializer, G: GroupGenerator, { - pub fn new( - field_rev: &Arc, - configuration: GroupConfigurationRevision, - cell_content_provider: &dyn GroupCellContentProvider, - ) -> FlowyResult { + pub fn new(field_rev: &Arc, configuration: GroupConfigurationRevision) -> FlowyResult { let configuration = match configuration.content { None => None, Some(content) => Some(C::try_from(Bytes::from(content))?), }; let field_type_rev = field_rev.field_type_rev; let type_option = field_rev.get_type_option_entry::(field_type_rev); - let groups = G::generate_groups(&configuration, &type_option, cell_content_provider); + let groups = G::generate_groups(&configuration, &type_option); let default_group = Group { id: DEFAULT_GROUP_ID.to_owned(), diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index 50628875a8..933e3f936e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -8,9 +8,7 @@ use std::sync::Arc; use crate::services::field::{ MultiSelectTypeOptionPB, SelectOptionCellDataPB, SelectOptionCellDataParser, SingleSelectTypeOptionPB, }; -use crate::services::group::{ - Group, GroupActionHandler, GroupCellContentProvider, GroupController, GroupGenerator, Groupable, -}; +use crate::services::group::{Group, GroupActionHandler, GroupController, GroupGenerator, Groupable}; // SingleSelect pub type SingleSelectGroupController = GroupController< @@ -59,7 +57,6 @@ impl GroupGenerator for SingleSelectGroupGenerator { fn generate_groups( _configuration: &Option, type_option: &Option, - _cell_content_provider: &dyn GroupCellContentProvider, ) -> Vec { match type_option { None => vec![], @@ -125,7 +122,6 @@ impl GroupGenerator for MultiSelectGroupGenerator { fn generate_groups( _configuration: &Option, type_option: &Option, - _cell_content_provider: &dyn GroupCellContentProvider, ) -> Vec { match type_option { None => vec![], diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index d03261079a..266abee6d7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -4,55 +4,42 @@ use crate::entities::{ NumberGroupConfigurationPB, RowPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, }; -use crate::services::block_manager::GridBlockManager; -use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::group::{ - CheckboxGroupController, GroupActionHandler, GroupCellContentProvider, MultiSelectGroupController, - SingleSelectGroupController, + CheckboxGroupController, GroupActionHandler, MultiSelectGroupController, SingleSelectGroupController, }; - use bytes::Bytes; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{gen_grid_group_id, FieldRevision, GroupConfigurationRevision, RowRevision}; -use flowy_sync::client_grid::GridRevisionPad; - +use lib_infra::future::AFFuture; +use std::future::Future; use std::sync::Arc; use tokio::sync::RwLock; -pub(crate) struct GridGroupService { - #[allow(dead_code)] - scheduler: Arc, - grid_pad: Arc>, - block_manager: Arc, - group_action_handler: Option>>, +pub trait GroupConfigurationDelegate: Send + Sync + 'static { + fn get_group_configuration(&self, field_rev: Arc) -> AFFuture; } -impl GridGroupService { - pub(crate) async fn new( - grid_pad: Arc>, - block_manager: Arc, - scheduler: S, - ) -> Self { - let scheduler = Arc::new(scheduler); +pub(crate) struct GroupService { + delegate: Box, + action_handler: Option>>, +} + +impl GroupService { + pub(crate) async fn new(delegate: Box) -> Self { Self { - scheduler, - grid_pad, - block_manager, - group_action_handler: None, + delegate, + action_handler: None, } } - pub(crate) async fn load_groups(&mut self) -> Option> { - let field_rev = find_group_field(self.grid_pad.read().await.fields()).unwrap(); + pub(crate) async fn load_groups( + &mut self, + field_revs: &[Arc], + row_revs: Vec>, + ) -> Option> { + let field_rev = find_group_field(field_revs).unwrap(); let field_type: FieldType = field_rev.field_type_rev.into(); - let configuration = self.get_group_configuration(&field_rev).await; - - let blocks = self.block_manager.get_block_snapshots(None).await.unwrap(); - let row_revs = blocks - .into_iter() - .map(|block| block.row_revs) - .flatten() - .collect::>>(); + let configuration = self.delegate.get_group_configuration(field_rev.clone()).await; match self .build_groups(&field_type, &field_rev, row_revs, configuration) @@ -63,36 +50,25 @@ impl GridGroupService { } } - #[tracing::instrument(level = "debug", skip(self, row_rev))] - pub(crate) async fn update_board_card(&self, row_rev: &mut RowRevision, group_id: &str) { - if let Some(group_action_handler) = self.group_action_handler.as_ref() { + pub(crate) async fn update_row(&self, row_rev: &mut RowRevision, group_id: &str, f: F) + where + F: FnOnce(String) -> O, + O: Future>> + Send + Sync + 'static, + { + if let Some(group_action_handler) = self.action_handler.as_ref() { let field_id = group_action_handler.read().await.field_id().to_owned(); - - match self.grid_pad.read().await.get_field_rev(&field_id) { - None => tracing::warn!("Fail to create card because the field does not exist"), - Some((_, field_rev)) => { + match f(field_id).await { + None => {} + Some(field_rev) => { group_action_handler .write() .await - .update_card(row_rev, field_rev, group_id); + .update_card(row_rev, &field_rev, group_id); } } } } - - pub(crate) async fn get_group_configuration(&self, field_rev: &FieldRevision) -> GroupConfigurationRevision { - let grid_pad = self.grid_pad.read().await; - let setting = grid_pad.get_setting_rev(); - let configurations = setting.get_groups(&field_rev.id, &field_rev.field_type_rev); - match configurations { - None => default_group_configuration(field_rev), - Some(mut configurations) => { - assert_eq!(configurations.len(), 1); - (&*configurations.pop().unwrap()).clone() - } - } - } - + #[allow(dead_code)] pub async fn move_card(&self, _group_id: &str, _from: i32, _to: i32) { // BoardCardChangesetPB { // group_id: "".to_string(), @@ -103,20 +79,20 @@ impl GridGroupService { // let row_pb = make_row_from_row_rev(row_rev); todo!() } - + #[allow(dead_code)] pub async fn did_delete_card(&self, _row_id: String) { // let changeset = BoardCardChangesetPB::delete(group_id.to_owned(), vec![row_id]); // self.notify_did_update_board(changeset).await; todo!() } - pub async fn did_create_card(&self, group_id: &str, row_pb: &RowPB) { + pub async fn did_create_row(&self, group_id: &str, row_pb: &RowPB) { let changeset = BoardCardChangesetPB::insert(group_id.to_owned(), vec![row_pb.clone()]); self.notify_did_update_board(changeset).await; } pub async fn notify_did_update_board(&self, changeset: BoardCardChangesetPB) { - if self.group_action_handler.is_none() { + if self.action_handler.is_none() { return; } send_dart_notification(&changeset.group_id, GridNotification::DidUpdateBoard) @@ -143,16 +119,16 @@ impl GridGroupService { // let generator = GroupGenerator::::from_configuration(configuration); } FieldType::SingleSelect => { - let controller = SingleSelectGroupController::new(field_rev, configuration, &self.grid_pad)?; - self.group_action_handler = Some(Arc::new(RwLock::new(controller))); + let controller = SingleSelectGroupController::new(field_rev, configuration)?; + self.action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::MultiSelect => { - let controller = MultiSelectGroupController::new(field_rev, configuration, &self.grid_pad)?; - self.group_action_handler = Some(Arc::new(RwLock::new(controller))); + let controller = MultiSelectGroupController::new(field_rev, configuration)?; + self.action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::Checkbox => { - let controller = CheckboxGroupController::new(field_rev, configuration, &self.grid_pad)?; - self.group_action_handler = Some(Arc::new(RwLock::new(controller))); + let controller = CheckboxGroupController::new(field_rev, configuration)?; + self.action_handler = Some(Arc::new(RwLock::new(controller))); } FieldType::URL => { // let generator = GroupGenerator::::from_configuration(configuration); @@ -160,7 +136,7 @@ impl GridGroupService { }; let mut groups = vec![]; - if let Some(group_action_handler) = self.group_action_handler.as_ref() { + if let Some(group_action_handler) = self.action_handler.as_ref() { let mut write_guard = group_action_handler.write().await; let _ = write_guard.group_rows(&row_revs, field_rev)?; groups = write_guard.get_groups(); @@ -182,9 +158,7 @@ fn find_group_field(field_revs: &[Arc]) -> Option> {} - -fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision { +pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision { let field_type: FieldType = field_rev.field_type_rev.into(); let bytes: Bytes = match field_type { FieldType::RichText => TextGroupConfigurationPB::default().try_into().unwrap(), diff --git a/frontend/rust-lib/flowy-grid/src/services/mod.rs b/frontend/rust-lib/flowy-grid/src/services/mod.rs index 2683d1f06a..2addb16686 100644 --- a/frontend/rust-lib/flowy-grid/src/services/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/mod.rs @@ -1,13 +1,14 @@ mod util; +pub mod block_editor; mod block_manager; -pub mod block_revision_editor; pub mod cell; pub mod field; mod filter; pub mod grid_editor; mod grid_editor_task; pub mod grid_view_editor; +pub mod grid_view_manager; pub mod group; pub mod persistence; pub mod row; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index f4e32e1406..2c54f8561e 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -2,7 +2,7 @@ use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow}; use crate::grid::block_test::util::GridRowTestBuilder; use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::{FieldType, GridCellIdParams, RowPB}; +use flowy_grid::entities::{CreateRowParams, FieldType, GridCellIdParams, RowPB}; use flowy_grid::services::field::*; use flowy_grid_data_model::revision::{ GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, @@ -77,7 +77,12 @@ impl GridRowTest { pub async fn run_script(&mut self, script: RowScript) { match script { RowScript::CreateEmptyRow => { - let row_order = self.editor.create_row(None).await.unwrap(); + let params = CreateRowParams { + grid_id: self.editor.grid_id.clone(), + start_row_id: None, + group_id: None, + }; + let row_order = self.editor.create_row(params).await.unwrap(); self.row_order_by_row_id .insert(row_order.row_id().to_owned(), row_order); self.row_revs = self.get_row_revs().await; diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs index 92525be9e0..31b69e0291 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/document_impl.rs @@ -1,5 +1,5 @@ use crate::cache::disk::RevisionDiskCache; -use crate::disk::{RevisionChangeset, RevisionRecord, RevisionState}; +use crate::disk::{RevisionChangeset, RevisionRecord}; use bytes::Bytes; use diesel::{sql_types::Integer, update, SqliteConnection}; use flowy_database::{ diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_impl.rs index 52b01440e6..2fcdb7e748 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_impl.rs @@ -1,5 +1,5 @@ use crate::cache::disk::RevisionDiskCache; -use crate::disk::{RevisionChangeset, RevisionRecord, RevisionState}; +use crate::disk::{RevisionChangeset, RevisionRecord}; use bytes::Bytes; use diesel::{sql_types::Integer, update, SqliteConnection}; use flowy_database::{ @@ -103,7 +103,7 @@ impl GridMetaRevisionSql { record.revision.object_id, record.revision.rev_id ); - let rev_state: GridMetaRevisionState = record.state.into(); + let rev_state: GridBlockRevisionState = record.state.into(); ( dsl::object_id.eq(record.revision.object_id), dsl::base_rev_id.eq(record.revision.base_rev_id), @@ -121,7 +121,7 @@ impl GridMetaRevisionSql { } fn update(changeset: RevisionChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> { - let state: GridMetaRevisionState = changeset.state.clone().into(); + let state: GridBlockRevisionState = changeset.state.clone().into(); let filter = dsl::grid_meta_rev_table .filter(dsl::rev_id.eq(changeset.rev_id.as_ref())) .filter(dsl::object_id.eq(changeset.object_id)); @@ -146,7 +146,7 @@ impl GridMetaRevisionSql { if let Some(rev_ids) = rev_ids { sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); } - let rows = sql.order(dsl::rev_id.asc()).load::(conn)?; + let rows = sql.order(dsl::rev_id.asc()).load::(conn)?; let records = rows .into_iter() .map(|row| mk_revision_record_from_table(user_id, row)) @@ -166,7 +166,7 @@ impl GridMetaRevisionSql { .filter(dsl::rev_id.le(range.end)) .filter(dsl::object_id.eq(object_id)) .order(dsl::rev_id.asc()) - .load::(conn)?; + .load::(conn)?; let revisions = rev_tables .into_iter() @@ -192,31 +192,31 @@ impl GridMetaRevisionSql { #[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)] #[table_name = "grid_meta_rev_table"] -struct GridMetaRevisionTable { +struct GridBlockRevisionTable { id: i32, object_id: String, base_rev_id: i64, rev_id: i64, data: Vec, - state: GridMetaRevisionState, + state: GridBlockRevisionState, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] #[repr(i32)] #[sql_type = "Integer"] -pub enum GridMetaRevisionState { +pub enum GridBlockRevisionState { Sync = 0, Ack = 1, } -impl_sql_integer_expression!(GridMetaRevisionState); -impl_rev_state_map!(GridMetaRevisionState); -impl std::default::Default for GridMetaRevisionState { +impl_sql_integer_expression!(GridBlockRevisionState); +impl_rev_state_map!(GridBlockRevisionState); +impl std::default::Default for GridBlockRevisionState { fn default() -> Self { - GridMetaRevisionState::Sync + GridBlockRevisionState::Sync } } -fn mk_revision_record_from_table(user_id: &str, table: GridMetaRevisionTable) -> RevisionRecord { +fn mk_revision_record_from_table(user_id: &str, table: GridBlockRevisionTable) -> RevisionRecord { let md5 = md5(&table.data); let revision = Revision::new( &table.object_id, diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_impl.rs index 9ddb21bc8c..bb6e561c91 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_impl.rs @@ -1,6 +1,5 @@ use crate::cache::disk::RevisionDiskCache; -use crate::disk::{RevisionChangeset, RevisionRecord, RevisionState}; - +use crate::disk::{RevisionChangeset, RevisionRecord}; use bytes::Bytes; use diesel::{sql_types::Integer, update, SqliteConnection}; use flowy_database::{ diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_view_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_view_impl.rs new file mode 100644 index 0000000000..2d7cbd59b2 --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_view_impl.rs @@ -0,0 +1,233 @@ +use crate::disk::{RevisionChangeset, RevisionDiskCache, RevisionRecord}; +use bytes::Bytes; +use diesel::{sql_types::Integer, update, SqliteConnection}; +use flowy_database::{ + impl_sql_integer_expression, insert_or_ignore_into, + prelude::*, + schema::{grid_view_rev_table, grid_view_rev_table::dsl}, + ConnectionPool, +}; +use flowy_error::{internal_error, FlowyError, FlowyResult}; +use flowy_sync::{ + entities::revision::{Revision, RevisionRange}, + util::md5, +}; +use std::sync::Arc; + +pub struct SQLiteGridViewRevisionPersistence { + user_id: String, + pub(crate) pool: Arc, +} + +impl SQLiteGridViewRevisionPersistence { + pub fn new(user_id: &str, pool: Arc) -> Self { + Self { + user_id: user_id.to_owned(), + pool, + } + } +} + +impl RevisionDiskCache for SQLiteGridViewRevisionPersistence { + type Error = FlowyError; + + fn create_revision_records(&self, revision_records: Vec) -> Result<(), Self::Error> { + let conn = self.pool.get().map_err(internal_error)?; + let _ = GridViewRevisionSql::create(revision_records, &*conn)?; + Ok(()) + } + + fn read_revision_records( + &self, + object_id: &str, + rev_ids: Option>, + ) -> Result, Self::Error> { + let conn = self.pool.get().map_err(internal_error)?; + let records = GridViewRevisionSql::read(&self.user_id, object_id, rev_ids, &*conn)?; + Ok(records) + } + + fn read_revision_records_with_range( + &self, + object_id: &str, + range: &RevisionRange, + ) -> Result, Self::Error> { + let conn = &*self.pool.get().map_err(internal_error)?; + let revisions = GridViewRevisionSql::read_with_range(&self.user_id, object_id, range.clone(), conn)?; + Ok(revisions) + } + + fn update_revision_record(&self, changesets: Vec) -> FlowyResult<()> { + let conn = &*self.pool.get().map_err(internal_error)?; + let _ = conn.immediate_transaction::<_, FlowyError, _>(|| { + for changeset in changesets { + let _ = GridViewRevisionSql::update(changeset, conn)?; + } + Ok(()) + })?; + Ok(()) + } + + fn delete_revision_records(&self, object_id: &str, rev_ids: Option>) -> Result<(), Self::Error> { + let conn = &*self.pool.get().map_err(internal_error)?; + let _ = GridViewRevisionSql::delete(object_id, rev_ids, conn)?; + Ok(()) + } + + fn delete_and_insert_records( + &self, + object_id: &str, + deleted_rev_ids: Option>, + inserted_records: Vec, + ) -> Result<(), Self::Error> { + let conn = self.pool.get().map_err(internal_error)?; + conn.immediate_transaction::<_, FlowyError, _>(|| { + let _ = GridViewRevisionSql::delete(object_id, deleted_rev_ids, &*conn)?; + let _ = GridViewRevisionSql::create(inserted_records, &*conn)?; + Ok(()) + }) + } +} + +struct GridViewRevisionSql(); +impl GridViewRevisionSql { + fn create(revision_records: Vec, conn: &SqliteConnection) -> Result<(), FlowyError> { + // Batch insert: https://diesel.rs/guides/all-about-inserts.html + let records = revision_records + .into_iter() + .map(|record| { + tracing::trace!( + "[GridViewRevisionSql] create revision: {}:{:?}", + record.revision.object_id, + record.revision.rev_id + ); + let rev_state: GridViewRevisionState = record.state.into(); + ( + dsl::object_id.eq(record.revision.object_id), + dsl::base_rev_id.eq(record.revision.base_rev_id), + dsl::rev_id.eq(record.revision.rev_id), + dsl::data.eq(record.revision.delta_data), + dsl::state.eq(rev_state), + ) + }) + .collect::>(); + + let _ = insert_or_ignore_into(dsl::grid_view_rev_table) + .values(&records) + .execute(conn)?; + Ok(()) + } + + fn update(changeset: RevisionChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> { + let state: GridViewRevisionState = changeset.state.clone().into(); + let filter = dsl::grid_view_rev_table + .filter(dsl::rev_id.eq(changeset.rev_id.as_ref())) + .filter(dsl::object_id.eq(changeset.object_id)); + let _ = update(filter).set(dsl::state.eq(state)).execute(conn)?; + tracing::debug!( + "[GridViewRevisionSql] update revision:{} state:to {:?}", + changeset.rev_id, + changeset.state + ); + Ok(()) + } + + fn read( + user_id: &str, + object_id: &str, + rev_ids: Option>, + conn: &SqliteConnection, + ) -> Result, FlowyError> { + let mut sql = dsl::grid_view_rev_table + .filter(dsl::object_id.eq(object_id)) + .into_boxed(); + if let Some(rev_ids) = rev_ids { + sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); + } + let rows = sql.order(dsl::rev_id.asc()).load::(conn)?; + let records = rows + .into_iter() + .map(|row| mk_revision_record_from_table(user_id, row)) + .collect::>(); + + Ok(records) + } + + fn read_with_range( + user_id: &str, + object_id: &str, + range: RevisionRange, + conn: &SqliteConnection, + ) -> Result, FlowyError> { + let rev_tables = dsl::grid_view_rev_table + .filter(dsl::rev_id.ge(range.start)) + .filter(dsl::rev_id.le(range.end)) + .filter(dsl::object_id.eq(object_id)) + .order(dsl::rev_id.asc()) + .load::(conn)?; + + let revisions = rev_tables + .into_iter() + .map(|table| mk_revision_record_from_table(user_id, table)) + .collect::>(); + Ok(revisions) + } + + fn delete(object_id: &str, rev_ids: Option>, conn: &SqliteConnection) -> Result<(), FlowyError> { + let mut sql = diesel::delete(dsl::grid_view_rev_table).into_boxed(); + sql = sql.filter(dsl::object_id.eq(object_id)); + + if let Some(rev_ids) = rev_ids { + tracing::trace!("[GridViewRevisionSql] Delete revision: {}:{:?}", object_id, rev_ids); + sql = sql.filter(dsl::rev_id.eq_any(rev_ids)); + } + + let affected_row = sql.execute(conn)?; + tracing::trace!("[GridViewRevisionSql] Delete {} rows", affected_row); + Ok(()) + } +} + +#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)] +#[table_name = "grid_view_rev_table"] +struct GridViewRevisionTable { + id: i32, + object_id: String, + base_rev_id: i64, + rev_id: i64, + data: Vec, + state: GridViewRevisionState, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)] +#[repr(i32)] +#[sql_type = "Integer"] +pub enum GridViewRevisionState { + Sync = 0, + Ack = 1, +} +impl_sql_integer_expression!(GridViewRevisionState); +impl_rev_state_map!(GridViewRevisionState); + +impl std::default::Default for GridViewRevisionState { + fn default() -> Self { + GridViewRevisionState::Sync + } +} + +fn mk_revision_record_from_table(user_id: &str, table: GridViewRevisionTable) -> RevisionRecord { + let md5 = md5(&table.data); + let revision = Revision::new( + &table.object_id, + table.base_rev_id, + table.rev_id, + Bytes::from(table.data), + user_id, + md5, + ); + RevisionRecord { + revision, + state: table.state.into(), + write_to_disk: false, + } +} diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs index 14614523ce..94c76fab11 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs @@ -1,10 +1,12 @@ mod document_impl; mod grid_block_impl; mod grid_impl; +mod grid_view_impl; pub use document_impl::*; pub use grid_block_impl::*; pub use grid_impl::*; +pub use grid_view_impl::*; use flowy_error::FlowyResult; use flowy_sync::entities::revision::{RevId, Revision, RevisionRange}; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index 691dd5e185..31e6265757 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -1,4 +1,4 @@ -use crate::revision::{GridBlockRevision, SettingRevision}; +use crate::revision::GridBlockRevision; use bytes::Bytes; use indexmap::IndexMap; use nanoid::nanoid; @@ -23,9 +23,6 @@ pub struct GridRevision { pub grid_id: String, pub fields: Vec>, pub blocks: Vec>, - - #[serde(default)] - pub setting: SettingRevision, } impl GridRevision { @@ -34,7 +31,6 @@ impl GridRevision { grid_id: grid_id.to_owned(), fields: vec![], blocks: vec![], - setting: SettingRevision::default(), } } @@ -43,7 +39,6 @@ impl GridRevision { grid_id: grid_id.to_owned(), fields: context.field_revs, blocks: context.blocks.into_iter().map(Arc::new).collect(), - setting: Default::default(), } } } diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs index 2b8861ba49..352b9e616c 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -2,6 +2,7 @@ use crate::revision::SettingRevision; use nanoid::nanoid; use serde::{Deserialize, Serialize}; +#[allow(dead_code)] pub fn gen_grid_view_id() -> String { nanoid!(6) } @@ -23,7 +24,7 @@ pub struct GridViewRevision { impl GridViewRevision { pub fn new(grid_id: String) -> Self { GridViewRevision { - view_id: gen_grid_view_id(), + view_id: grid_id.clone(), grid_id, setting: Default::default(), row_orders: vec![], diff --git a/shared-lib/flowy-sync/src/client_folder/builder.rs b/shared-lib/flowy-sync/src/client_folder/builder.rs index 75a1343570..6581591c25 100644 --- a/shared-lib/flowy-sync/src/client_folder/builder.rs +++ b/shared-lib/flowy-sync/src/client_folder/builder.rs @@ -1,5 +1,5 @@ use crate::entities::folder::FolderDelta; -use crate::util::make_delta_from_revisions; +use crate::util::make_text_delta_from_revisions; use crate::{ client_folder::{default_folder_delta, FolderPad}, entities::revision::Revision, @@ -7,7 +7,7 @@ use crate::{ }; use flowy_folder_data_model::revision::{TrashRevision, WorkspaceRevision}; -use lib_ot::core::PhantomAttributes; + use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -37,7 +37,7 @@ impl FolderPadBuilder { } pub(crate) fn build_with_revisions(self, revisions: Vec) -> CollaborateResult { - let mut folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; + let mut folder_delta: FolderDelta = make_text_delta_from_revisions(revisions)?; if folder_delta.is_empty() { folder_delta = default_folder_delta(); } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index 25a7de66a8..9b471a206b 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -1,14 +1,11 @@ -use crate::entities::grid::{ - CreateGridFilterParams, CreateGridGroupParams, FieldChangesetParams, GridSettingChangesetParams, -}; +use crate::entities::grid::FieldChangesetParams; use crate::entities::revision::{md5, RepeatedRevision, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; -use crate::util::{cal_diff, make_delta_from_revisions}; +use crate::util::{cal_diff, make_text_delta_from_revisions}; use bytes::Bytes; use flowy_grid_data_model::revision::{ - gen_block_id, gen_grid_filter_id, gen_grid_group_id, gen_grid_id, FieldRevision, FieldTypeRevision, - FilterConfigurationRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, GridRevision, - GroupConfigurationRevision, SettingRevision, + gen_block_id, gen_grid_id, FieldRevision, FieldTypeRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, + GridRevision, }; use lib_infra::util::move_vec_element; use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; @@ -68,7 +65,7 @@ impl GridRevisionPad { } pub fn from_revisions(revisions: Vec) -> CollaborateResult { - let grid_delta: GridRevisionDelta = make_delta_from_revisions::(revisions)?; + let grid_delta: GridRevisionDelta = make_text_delta_from_revisions(revisions)?; Self::from_delta(grid_delta) } @@ -347,95 +344,6 @@ impl GridRevisionPad { }) } - pub fn get_setting_rev(&self) -> &SettingRevision { - &self.grid_rev.setting - } - - /// If layout is None, then the default layout will be the read from GridSettingRevision - pub fn get_filters(&self, field_ids: Option>) -> Option>> { - let mut filter_revs = vec![]; - let field_revs = self.get_field_revs(None).ok()?; - - field_revs.iter().for_each(|field_rev| { - let mut is_contain = true; - if let Some(field_ids) = &field_ids { - is_contain = field_ids.contains(&field_rev.id); - } - - if is_contain { - // Only return the filters for the current fields' type. - let field_id = &field_rev.id; - let field_type_rev = &field_rev.field_type_rev; - if let Some(mut t_filter_revs) = self.grid_rev.setting.get_filters(field_id, field_type_rev) { - filter_revs.append(&mut t_filter_revs); - } - } - }); - - Some(filter_revs) - } - - pub fn update_grid_setting_rev( - &mut self, - changeset: GridSettingChangesetParams, - ) -> CollaborateResult> { - self.modify_grid(|grid_rev| { - let mut is_changed = None; - if let Some(params) = changeset.insert_filter { - grid_rev - .setting - .insert_filter(¶ms.field_id, ¶ms.field_type_rev, make_filter_revision(¶ms)); - is_changed = Some(()) - } - if let Some(params) = changeset.delete_filter { - if let Some(filters) = grid_rev - .setting - .get_mut_filters(¶ms.field_id, ¶ms.field_type_rev) - { - filters.retain(|filter| filter.id != params.filter_id); - } - } - if let Some(params) = changeset.insert_group { - grid_rev - .setting - .insert_group(¶ms.field_id, ¶ms.field_type_rev, make_group_revision(¶ms)); - is_changed = Some(()); - } - if let Some(params) = changeset.delete_group { - if let Some(groups) = grid_rev - .setting - .get_mut_groups(¶ms.field_id, ¶ms.field_type_rev) - { - groups.retain(|filter| filter.id != params.group_id); - } - } - if let Some(_sort) = changeset.insert_sort { - // let rev = GridSortRevision { - // id: gen_grid_sort_id(), - // field_id: sort.field_id, - // }; - // - // grid_rev - // .setting - // .sorts - // .entry(layout_rev.clone()) - // .or_insert_with(std::vec::Vec::new) - // .push(rev); - is_changed = Some(()) - } - - if let Some(_delete_sort_id) = changeset.delete_sort { - // match grid_rev.setting.sorts.get_mut(&layout_rev) { - // Some(sorts) => sorts.retain(|sort| sort.id != delete_sort_id), - // None => { - // tracing::warn!("Can't find the sort with {:?}", layout_rev); - // } - // } - } - Ok(is_changed) - }) - } - pub fn md5(&self) -> String { md5(&self.delta.json_bytes()) } @@ -548,21 +456,3 @@ impl std::default::Default for GridRevisionPad { } } } - -fn make_filter_revision(params: &CreateGridFilterParams) -> FilterConfigurationRevision { - FilterConfigurationRevision { - id: gen_grid_filter_id(), - field_id: params.field_id.clone(), - condition: params.condition, - content: params.content.clone(), - } -} - -fn make_group_revision(params: &CreateGridGroupParams) -> GroupConfigurationRevision { - GroupConfigurationRevision { - id: gen_grid_group_id(), - field_id: params.field_id.clone(), - field_type_rev: params.field_type_rev, - content: params.content.clone(), - } -} diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index a3906a4aaf..fadd6d57a2 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -1,9 +1,11 @@ +use crate::entities::grid::{CreateGridFilterParams, CreateGridGroupParams, GridSettingChangesetParams}; use crate::entities::revision::{md5, Revision}; use crate::errors::{internal_error, CollaborateError, CollaborateResult}; use crate::util::{cal_diff, make_text_delta_from_revisions}; use flowy_grid_data_model::revision::{ - FieldRevision, FieldTypeRevision, FilterConfigurationRevision, FilterConfigurationsByFieldId, GridViewRevision, - GroupConfigurationRevision, GroupConfigurationsByFieldId, SortConfigurationsByFieldId, + gen_grid_filter_id, gen_grid_group_id, FieldRevision, FieldTypeRevision, FilterConfigurationRevision, + FilterConfigurationsByFieldId, GridViewRevision, GroupConfigurationRevision, GroupConfigurationsByFieldId, + SettingRevision, SortConfigurationsByFieldId, }; use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::sync::Arc; @@ -48,6 +50,59 @@ impl GridViewRevisionPad { Self::from_delta(delta) } + pub fn get_setting_rev(&self) -> &SettingRevision { + &self.view.setting + } + + pub fn update_setting( + &mut self, + changeset: GridSettingChangesetParams, + ) -> CollaborateResult> { + self.modify(|view| { + let mut is_changed = None; + if let Some(params) = changeset.insert_filter { + view.setting.filters.insert_object( + ¶ms.field_id, + ¶ms.field_type_rev, + make_filter_revision(¶ms), + ); + is_changed = Some(()) + } + if let Some(params) = changeset.delete_filter { + if let Some(filters) = view + .setting + .filters + .get_mut_objects(¶ms.field_id, ¶ms.field_type_rev) + { + filters.retain(|filter| filter.id != params.filter_id); + is_changed = Some(()) + } + } + if let Some(params) = changeset.insert_group { + view.setting.groups.remove_all(); + view.setting.groups.insert_object( + ¶ms.field_id, + ¶ms.field_type_rev, + make_group_revision(¶ms), + ); + + is_changed = Some(()); + } + if let Some(params) = changeset.delete_group { + if let Some(groups) = view + .setting + .groups + .get_mut_objects(¶ms.field_id, ¶ms.field_type_rev) + { + groups.retain(|group| group.id != params.group_id); + is_changed = Some(()); + } + } + + Ok(is_changed) + }) + } + pub fn get_all_groups(&self, field_revs: &[Arc]) -> Option { self.setting.groups.get_all_objects(field_revs) } @@ -161,6 +216,24 @@ impl GridViewRevisionPad { } } +fn make_filter_revision(params: &CreateGridFilterParams) -> FilterConfigurationRevision { + FilterConfigurationRevision { + id: gen_grid_filter_id(), + field_id: params.field_id.clone(), + condition: params.condition, + content: params.content.clone(), + } +} + +fn make_group_revision(params: &CreateGridGroupParams) -> GroupConfigurationRevision { + GroupConfigurationRevision { + id: gen_grid_group_id(), + field_id: params.field_id.clone(), + field_type_rev: params.field_type_rev, + content: params.content.clone(), + } +} + pub struct GridViewRevisionChangeset { pub delta: TextDelta, pub md5: String, diff --git a/shared-lib/lib-infra/src/future.rs b/shared-lib/lib-infra/src/future.rs index 9077dd18b7..a6bad3b298 100644 --- a/shared-lib/lib-infra/src/future.rs +++ b/shared-lib/lib-infra/src/future.rs @@ -8,20 +8,20 @@ use std::{ task::{Context, Poll}, }; -pub fn wrap_future(f: T) -> FnFuture +pub fn wrap_future(f: T) -> AFFuture where T: Future + Send + Sync + 'static, { - FnFuture { fut: Box::pin(f) } + AFFuture { fut: Box::pin(f) } } #[pin_project] -pub struct FnFuture { +pub struct AFFuture { #[pin] pub fut: Pin + Sync + Send>>, } -impl Future for FnFuture +impl Future for AFFuture where T: Send + Sync, { From f841587c27342fb41b63cfef3dfb8148d836f4fc Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 15 Aug 2022 22:40:54 +0800 Subject: [PATCH 150/224] chore: add log --- frontend/rust-lib/flowy-error/src/errors.rs | 2 ++ .../flowy-folder/src/services/app/controller.rs | 4 +++- .../flowy-folder/src/services/app/event_handler.rs | 2 +- frontend/rust-lib/flowy-test/src/event_builder.rs | 14 ++++++++++---- .../rust-lib/flowy-user/src/services/database.rs | 2 +- frontend/rust-lib/lib-dispatch/src/byte_trait.rs | 7 ++++++- frontend/rust-lib/lib-dispatch/src/data.rs | 11 +++++++++-- frontend/rust-lib/lib-dispatch/src/dispatcher.rs | 12 +++++++----- 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index cd890fd02e..e19ebb15b2 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -93,6 +93,8 @@ impl fmt::Display for FlowyError { impl lib_dispatch::Error for FlowyError { fn as_response(&self) -> EventResponse { let bytes: Bytes = self.clone().try_into().unwrap(); + + println!("Serialize FlowyError: {:?} to event response", self); ResponseBuilder::Err().data(bytes).build() } } diff --git a/frontend/rust-lib/flowy-folder/src/services/app/controller.rs b/frontend/rust-lib/flowy-folder/src/services/app/controller.rs index 112ad052ce..375e696bd1 100644 --- a/frontend/rust-lib/flowy-folder/src/services/app/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/app/controller.rs @@ -68,7 +68,9 @@ impl AppController { let app = transaction.read_app(¶ms.value)?; let trash_ids = self.trash_controller.read_trash_ids(&transaction)?; if trash_ids.contains(&app.id) { - return Err(FlowyError::record_not_found()); + return Err( + FlowyError::record_not_found().context(format!("Can not find the app:{}", params.value)) + ); } Ok(app) }) diff --git a/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs index e2087c167e..1594351015 100644 --- a/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs @@ -44,7 +44,7 @@ pub(crate) async fn update_app_handler( Ok(()) } -#[tracing::instrument(level = "trace", skip(data, app_controller, view_controller))] +#[tracing::instrument(level = "info", skip(data, app_controller, view_controller), err)] pub(crate) async fn read_app_handler( data: Data, app_controller: AppData>, diff --git a/frontend/rust-lib/flowy-test/src/event_builder.rs b/frontend/rust-lib/flowy-test/src/event_builder.rs index d6f42ecf31..75cdff5f38 100644 --- a/frontend/rust-lib/flowy-test/src/event_builder.rs +++ b/frontend/rust-lib/flowy-test/src/event_builder.rs @@ -83,15 +83,21 @@ where R: FromBytes, { let response = self.get_response(); - match response.parse::() { + match response.clone().parse::() { Ok(Ok(data)) => data, Ok(Err(e)) => { - panic!("Parser {:?} failed: {:?}", std::any::type_name::(), e) + panic!( + "Parser {:?} failed: {:?}, response {:?}", + std::any::type_name::(), + e, + response + ) } Err(e) => panic!( - "Internal error: {:?}, parser {:?} failed", - e, + "Dispatch {:?} failed: {:?}, response {:?}", std::any::type_name::(), + e, + response ), } } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index 62b74c9f22..9e94ea2c9d 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -37,8 +37,8 @@ impl UserDB { Some(database) => return Ok(database.get_pool()), } - tracing::trace!("open user db {}", user_id); let dir = format!("{}/{}", self.db_dir, user_id); + tracing::trace!("open user db {} at path: {}", user_id, dir); let db = flowy_database::init(&dir).map_err(|e| { log::error!("open user: {} db failed, {:?}", user_id, e); FlowyError::internal().context(e) diff --git a/frontend/rust-lib/lib-dispatch/src/byte_trait.rs b/frontend/rust-lib/lib-dispatch/src/byte_trait.rs index 9f8c2a6545..0e48813afd 100644 --- a/frontend/rust-lib/lib-dispatch/src/byte_trait.rs +++ b/frontend/rust-lib/lib-dispatch/src/byte_trait.rs @@ -14,7 +14,12 @@ where fn into_bytes(self) -> Result { match self.try_into() { Ok(data) => Ok(data), - Err(e) => Err(InternalError::ProtobufError(format!("{:?}", e)).into()), + Err(e) => Err(InternalError::ProtobufError(format!( + "Serial {:?} to bytes failed:{:?}", + std::any::type_name::(), + e + )) + .into()), } } } diff --git a/frontend/rust-lib/lib-dispatch/src/data.rs b/frontend/rust-lib/lib-dispatch/src/data.rs index e331fe071e..15f9684a16 100644 --- a/frontend/rust-lib/lib-dispatch/src/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/data.rs @@ -55,7 +55,10 @@ where { fn respond_to(self, _request: &EventRequest) -> EventResponse { match self.into_inner().into_bytes() { - Ok(bytes) => ResponseBuilder::Ok().data(bytes).build(), + Ok(bytes) => { + log::trace!("Serialize Data: {:?} to event response", std::any::type_name::()); + return ResponseBuilder::Ok().data(bytes).build(); + } Err(e) => e.into(), } } @@ -86,7 +89,11 @@ where T: FromBytes, { match payload { - Payload::None => Err(InternalError::UnexpectedNone("Parse fail, expected payload".to_string()).into()), + Payload::None => Err(InternalError::UnexpectedNone(format!( + "Parse fail, expected payload:{:?}", + std::any::type_name::() + )) + .into()), Payload::Bytes(bytes) => { let data = T::parse_from_bytes(bytes.clone())?; Ok(Data(data)) diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index fd7296a70d..961f19986b 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -54,16 +54,18 @@ impl EventDispatcher { callback: Some(Box::new(callback)), }; let join_handle = dispatch.runtime.spawn(async move { - service - .call(service_ctx) - .await - .unwrap_or_else(|e| InternalError::Other(format!("{:?}", e)).as_response()) + service.call(service_ctx).await.unwrap_or_else(|e| { + tracing::error!("Dispatch runtime error: {:?}", e); + InternalError::Other(format!("{:?}", e)).as_response() + }) }); DispatchFuture { fut: Box::pin(async move { join_handle.await.unwrap_or_else(|e| { - let error = InternalError::JoinError(format!("EVENT_DISPATCH join error: {:?}", e)); + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); error.as_response() }) }), From 461160094c63d8f70493fc70f11a48f7feddc890 Mon Sep 17 00:00:00 2001 From: appflowy Date: Mon, 15 Aug 2022 23:17:29 +0800 Subject: [PATCH 151/224] chore: create grid view when create grid --- .../app_flowy/lib/plugins/board/board.dart | 2 +- frontend/rust-lib/flowy-grid/src/manager.rs | 35 ++++++++++++++----- .../src/services/grid_view_manager.rs | 23 +++++++++--- .../src/revision/grid_view.rs | 4 +-- .../flowy-grid-data-model/tests/serde_test.rs | 5 +-- .../src/client_grid/view_revision_pad.rs | 9 +++-- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/board.dart b/frontend/app_flowy/lib/plugins/board/board.dart index 36d181ae3e..2954a7cbf9 100644 --- a/frontend/app_flowy/lib/plugins/board/board.dart +++ b/frontend/app_flowy/lib/plugins/board/board.dart @@ -31,7 +31,7 @@ class BoardPluginBuilder implements PluginBuilder { class BoardPluginConfig implements PluginConfig { @override - bool get creatable => true; + bool get creatable => false; } class BoardPlugin extends Plugin { diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index f99e6ca4c6..4b4ea3b167 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -1,5 +1,6 @@ use crate::services::block_editor::GridBlockRevisionCompactor; use crate::services::grid_editor::{GridRevisionCompactor, GridRevisionEditor}; +use crate::services::grid_view_manager::make_grid_view_rev_manager; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::persistence::kv::GridKVPersistence; use crate::services::persistence::migration::GridMigration; @@ -9,10 +10,10 @@ use bytes::Bytes; use dashmap::DashMap; use flowy_database::ConnectionPool; use flowy_error::{FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{BuildGridContext, GridRevision}; +use flowy_grid_data_model::revision::{BuildGridContext, GridRevision, GridViewRevision}; use flowy_revision::disk::{SQLiteGridBlockRevisionPersistence, SQLiteGridRevisionPersistence}; use flowy_revision::{RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence}; -use flowy_sync::client_grid::{make_grid_block_delta, make_grid_delta}; +use flowy_sync::client_grid::{make_grid_block_delta, make_grid_delta, make_grid_view_delta}; use flowy_sync::entities::revision::{RepeatedRevision, Revision}; use std::sync::Arc; use tokio::sync::RwLock; @@ -70,6 +71,15 @@ impl GridManager { let db_pool = self.grid_user.db_pool()?; let rev_manager = self.make_grid_rev_manager(grid_id, db_pool)?; let _ = rev_manager.reset_object(revisions).await?; + + Ok(()) + } + + #[tracing::instrument(level = "debug", skip_all, err)] + async fn create_grid_view>(&self, view_id: T, revisions: RepeatedRevision) -> FlowyResult<()> { + let view_id = view_id.as_ref(); + let rev_manager = make_grid_view_rev_manager(&self.grid_user, view_id).await?; + let _ = rev_manager.reset_object(revisions).await?; Ok(()) } @@ -198,14 +208,23 @@ pub async fn make_grid_view_data( let _ = grid_manager.create_grid_block(&block_id, repeated_revision).await?; } - let grid_rev = GridRevision::from_build_context(view_id, build_context); + let grid_id = view_id.to_owned(); + let grid_rev = GridRevision::from_build_context(&grid_id, build_context); // Create grid - let grid_meta_delta = make_grid_delta(&grid_rev); - let grid_delta_data = grid_meta_delta.json_bytes(); + let grid_rev_delta = make_grid_delta(&grid_rev); + let grid_rev_delta_bytes = grid_rev_delta.json_bytes(); let repeated_revision: RepeatedRevision = - Revision::initial_revision(user_id, view_id, grid_delta_data.clone()).into(); - let _ = grid_manager.create_grid(view_id, repeated_revision).await?; + Revision::initial_revision(user_id, &grid_id, grid_rev_delta_bytes.clone()).into(); + let _ = grid_manager.create_grid(&grid_id, repeated_revision).await?; - Ok(grid_delta_data) + // Create grid view + let grid_view = GridViewRevision::new(view_id.to_owned(), view_id.to_owned()); + let grid_view_delta = make_grid_view_delta(&grid_view); + let grid_view_delta_bytes = grid_view_delta.json_bytes(); + let repeated_revision: RepeatedRevision = + Revision::initial_revision(user_id, view_id, grid_view_delta_bytes).into(); + let _ = grid_manager.create_grid_view(view_id, repeated_revision).await?; + + Ok(grid_rev_delta_bytes) } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index 4daf2af667..70768ef3a4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -140,18 +140,31 @@ where DataSource: GridViewRevisionDataSource, { tracing::trace!("Open view:{} editor", view_id); + + let rev_manager = make_grid_view_rev_manager(user, view_id).await?; + let user_id = user.user_id()?; let token = user.token()?; + let view_id = view_id.to_owned(); + GridViewRevisionEditor::new(&user_id, &token, view_id, delegate, data_source, scheduler, rev_manager).await +} + +pub async fn make_grid_view_rev_manager(user: &Arc, view_id: &str) -> FlowyResult { + tracing::trace!("Open view:{} editor", view_id); let user_id = user.user_id()?; let pool = user.db_pool()?; - let view_id = view_id.to_owned(); let disk_cache = SQLiteGridViewRevisionPersistence::new(&user_id, pool.clone()); - let rev_persistence = RevisionPersistence::new(&user_id, &view_id, disk_cache); + let rev_persistence = RevisionPersistence::new(&user_id, view_id, disk_cache); let rev_compactor = GridViewRevisionCompactor(); - let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(&view_id, pool); - let rev_manager = RevisionManager::new(&user_id, &view_id, rev_persistence, rev_compactor, snapshot_persistence); - GridViewRevisionEditor::new(&user_id, &token, view_id, delegate, data_source, scheduler, rev_manager).await + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(view_id, pool); + Ok(RevisionManager::new( + &user_id, + view_id, + rev_persistence, + rev_compactor, + snapshot_persistence, + )) } pub struct GridViewRevisionCompactor(); diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs index 352b9e616c..2fcc2fc4eb 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_view.rs @@ -22,9 +22,9 @@ pub struct GridViewRevision { } impl GridViewRevision { - pub fn new(grid_id: String) -> Self { + pub fn new(grid_id: String, view_id: String) -> Self { GridViewRevision { - view_id: grid_id.clone(), + view_id, grid_id, setting: Default::default(), row_orders: vec![], diff --git a/shared-lib/flowy-grid-data-model/tests/serde_test.rs b/shared-lib/flowy-grid-data-model/tests/serde_test.rs index 8a630a4651..b544e10588 100644 --- a/shared-lib/flowy-grid-data-model/tests/serde_test.rs +++ b/shared-lib/flowy-grid-data-model/tests/serde_test.rs @@ -6,8 +6,5 @@ fn grid_default_serde_test() { let grid = GridRevision::new(&grid_id); let json = serde_json::to_string(&grid).unwrap(); - assert_eq!( - json, - r#"{"grid_id":"1","fields":[],"blocks":[],"setting":{"layout":0,"filters":[],"groups":[]}}"# - ) + assert_eq!(json, r#"{"grid_id":"1","fields":[],"blocks":[]}"#) } diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index fadd6d57a2..dd6cc6f977 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -25,8 +25,8 @@ impl std::ops::Deref for GridViewRevisionPad { } impl GridViewRevisionPad { - pub fn new(grid_id: String) -> Self { - let view = Arc::new(GridViewRevision::new(grid_id)); + pub fn new(grid_id: String, view_id: String) -> Self { + let view = Arc::new(GridViewRevision::new(grid_id, view_id)); let json = serde_json::to_string(&view).unwrap(); let delta = TextDeltaBuilder::new().insert(&json).build(); Self { view, delta } @@ -244,3 +244,8 @@ pub fn make_grid_view_rev_json_str(grid_revision: &GridViewRevision) -> Collabor .map_err(|err| internal_error(format!("Serialize grid view to json str failed. {:?}", err)))?; Ok(json) } + +pub fn make_grid_view_delta(grid_view: &GridViewRevision) -> TextDelta { + let json = serde_json::to_string(grid_view).unwrap(); + TextDeltaBuilder::new().insert(&json).build() +} From 88ae437379a0f62dd5b15d8f06be5aeae2c629af Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 15 Aug 2022 23:29:42 +0800 Subject: [PATCH 152/224] test: update text style by command + x --- .../src/extensions/text_node_extensions.dart | 24 +++++++++ ..._text_style_by_command_x_handler_test.dart | 49 +++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart index 3408546c42..315f529710 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/extensions/text_node_extensions.dart @@ -41,6 +41,30 @@ extension TextNodeExtension on TextNode { } return true; } + + bool allNotSatisfyInSelection(String styleKey, Selection selection) { + final ops = delta.whereType(); + final startOffset = + selection.isBackward ? selection.start.offset : selection.end.offset; + final endOffset = + selection.isBackward ? selection.end.offset : selection.start.offset; + var start = 0; + for (final op in ops) { + if (start >= endOffset) { + break; + } + final length = op.length; + if (start < endOffset && start + length > startOffset) { + if (op.attributes != null && + op.attributes!.containsKey(styleKey) && + op.attributes![styleKey] == true) { + return false; + } + } + start += length; + } + return true; + } } extension TextNodesExtension on List { diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index 39c750a933..e91f089def 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -18,7 +18,6 @@ void main() async { LogicalKeyboardKey.keyB, ); }); - testWidgets('Presses Command + I to update text style', (tester) async { await _testUpdateTextStyleByCommandX( tester, @@ -26,7 +25,6 @@ void main() async { LogicalKeyboardKey.keyI, ); }); - testWidgets('Presses Command + U to update text style', (tester) async { await _testUpdateTextStyleByCommandX( tester, @@ -34,7 +32,6 @@ void main() async { LogicalKeyboardKey.keyU, ); }); - testWidgets('Presses Command + S to update text style', (tester) async { await _testUpdateTextStyleByCommandX( tester, @@ -83,5 +80,49 @@ Future _testUpdateTextStyleByCommandX( isMetaPressed: true, ); textNode = editor.nodeAtPath([1]) as TextNode; - expect(textNode.allSatisfyInSelection(matchStyle, selection), false); + expect(textNode.allNotSatisfyInSelection(matchStyle, selection), true); + + selection = Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [2], offset: text.length), + ); + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + var nodes = editor.editorState.service.selectionService.currentSelectedNodes + .whereType(); + expect(nodes.length, 3); + for (final node in nodes) { + expect( + node.allSatisfyInSelection( + matchStyle, + Selection.single( + path: node.path, startOffset: 0, endOffset: text.length), + ), + true, + ); + } + + await editor.updateSelection(selection); + await editor.pressLogicKey( + key, + isShiftPressed: key == LogicalKeyboardKey.keyS, + isMetaPressed: true, + ); + nodes = editor.editorState.service.selectionService.currentSelectedNodes + .whereType(); + expect(nodes.length, 3); + for (final node in nodes) { + expect( + node.allNotSatisfyInSelection( + matchStyle, + Selection.single( + path: node.path, startOffset: 0, endOffset: text.length), + ), + true, + ); + } } From a7681f86e51291996ca456bb906e1e8f681c6d0f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 00:14:40 +0800 Subject: [PATCH 153/224] test: implement white_space_handler test --- .../whitespace_handler.dart | 7 + .../flowy_editor/lib/src/service/service.dart | 12 +- .../flowy_editor/test/infra/test_editor.dart | 25 +++ .../test/infra/test_raw_key_event.dart | 3 + .../white_space_handler_test.dart | 178 ++++++++++++++++++ 5 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart index 0deb3d44d2..0b0b834936 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart @@ -9,6 +9,13 @@ import 'package:flowy_editor/src/operation/transaction_builder.dart'; import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/src/service/keyboard_service.dart'; +@visibleForTesting +List get checkboxListSymbols => _checkboxListSymbols; +@visibleForTesting +List get unCheckboxListSymbols => _unCheckboxListSymbols; +@visibleForTesting +List get bulletedListSymbols => _bulletedListSymbols; + const _bulletedListSymbols = ['*', '-']; const _checkboxListSymbols = ['[x]', '-[x]']; const _unCheckboxListSymbols = ['[]', '-[]']; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart index fc2e4e3f31..158aab4615 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/service.dart @@ -1,7 +1,4 @@ -import 'package:flowy_editor/src/service/keyboard_service.dart'; -import 'package:flowy_editor/src/service/render_plugin_service.dart'; -import 'package:flowy_editor/src/service/scroll_service.dart'; -import 'package:flowy_editor/src/service/selection_service.dart'; +import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/src/service/toolbar_service.dart'; import 'package:flutter/material.dart'; @@ -26,6 +23,13 @@ class FlowyService { // input service final inputServiceKey = GlobalKey(debugLabel: 'flowy_input_service'); + FlowyInputService? get inputService { + if (inputServiceKey.currentState != null && + inputServiceKey.currentState is FlowyInputService) { + return inputServiceKey.currentState! as FlowyInputService; + } + return null; + } // render plugin service late FlowyRenderPlugin renderPluginService; diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index 533cace586..61ece83c5a 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -70,6 +70,31 @@ class EditorWidgetTester { _editorState.service.selectionService.updateSelection(selection); } await tester.pumpAndSettle(); + + expect(_editorState.service.selectionService.currentSelection.value, + selection); + } + + Future insertText(TextNode textNode, String text, int offset, + {Selection? selection}) async { + await apply([ + TextEditingDeltaInsertion( + oldText: textNode.toRawString(), + textInserted: text, + insertionOffset: offset, + selection: selection != null + ? TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset) + : TextSelection.collapsed(offset: offset), + composing: TextRange.empty, + ) + ]); + } + + Future apply(List deltas) async { + _editorState.service.inputService?.apply(deltas); + await tester.pumpAndSettle(); } Future pressLogicKey( diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart index e4eb99b60e..04b5a11789 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_raw_key_event.dart @@ -79,6 +79,9 @@ extension on LogicalKeyboardKey { if (this == LogicalKeyboardKey.enter) { return PhysicalKeyboardKey.enter; } + if (this == LogicalKeyboardKey.space) { + return PhysicalKeyboardKey.space; + } if (this == LogicalKeyboardKey.backspace) { return PhysicalKeyboardKey.backspace; } diff --git a/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart new file mode 100644 index 0000000000..fb4c187f0f --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart @@ -0,0 +1,178 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('white_space_handler.dart', () { + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // [h1]Welcome to Appflowy 😁 + // [h2]Welcome to Appflowy 😁 + // [h3]Welcome to Appflowy 😁 + // [h4]Welcome to Appflowy 😁 + // [h5]Welcome to Appflowy 😁 + // [h6]Welcome to Appflowy 😁 + // + testWidgets('Presses whitespace key after #*', (tester) async { + const maxSignCount = 6; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 1; i <= maxSignCount; i++) { + editor.insertTextNode('${'#' * i}$text'); + } + await editor.startTesting(); + + for (var i = 1; i <= maxSignCount; i++) { + await editor.updateSelection( + Selection.single(path: [i - 1], startOffset: i), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + + final textNode = (editor.nodeAtPath([i - 1]) as TextNode); + + expect(textNode.subtype, StyleKey.heading); + // StyleKey.h1 ~ StyleKey.h6 + expect(textNode.attributes.heading, 'h$i'); + } + }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // [h1]##Welcome to Appflowy 😁 + // [h2]##Welcome to Appflowy 😁 + // [h3]##Welcome to Appflowy 😁 + // [h4]##Welcome to Appflowy 😁 + // [h5]##Welcome to Appflowy 😁 + // [h6]##Welcome to Appflowy 😁 + // + testWidgets('Presses whitespace key inside #*', (tester) async { + const maxSignCount = 6; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 1; i <= maxSignCount; i++) { + editor.insertTextNode('${'###' * i}$text'); + } + await editor.startTesting(); + + for (var i = 1; i <= maxSignCount; i++) { + await editor.updateSelection( + Selection.single(path: [i - 1], startOffset: i), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + + final textNode = (editor.nodeAtPath([i - 1]) as TextNode); + + expect(textNode.subtype, StyleKey.heading); + // StyleKey.h1 ~ StyleKey.h6 + expect(textNode.attributes.heading, 'h$i'); + expect(textNode.toRawString().startsWith('##'), true); + } + }); + + // Before + // + // Welcome to Appflowy 😁 + // + // After + // [h1 ~ h6]##Welcome to Appflowy 😁 + // + testWidgets('Presses whitespace key in heading styled text', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + + await editor.startTesting(); + + const maxSignCount = 6; + for (var i = 1; i <= maxSignCount; i++) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + + final textNode = (editor.nodeAtPath([0]) as TextNode); + + await editor.insertText(textNode, '#' * i, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + + expect(textNode.subtype, StyleKey.heading); + // StyleKey.h2 ~ StyleKey.h6 + expect(textNode.attributes.heading, 'h$i'); + } + }); + + testWidgets('Presses whitespace key after (un)checkbox symbols', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + for (final symbol in unCheckboxListSymbols) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.insertText(textNode, symbol, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, StyleKey.checkbox); + expect(textNode.attributes.check, false); + } + }); + + testWidgets('Presses whitespace key after checkbox symbols', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + for (final symbol in checkboxListSymbols) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.insertText(textNode, symbol, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, StyleKey.checkbox); + expect(textNode.attributes.check, true); + } + }); + + testWidgets('Presses whitespace key after bulleted list', (tester) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + for (final symbol in bulletedListSymbols) { + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.insertText(textNode, symbol, 0); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect(textNode.subtype, StyleKey.bulletedList); + } + }); + }); +} From 4683dbee45bd6904949dfe71becae0b17884751d Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 11:24:37 +0800 Subject: [PATCH 154/224] chore: add revision reset helper --- frontend/rust-lib/Cargo.lock | 1 + .../src/services/persistence/migration.rs | 80 ++++++++---- .../src/services/persistence/mod.rs | 5 +- frontend/rust-lib/flowy-grid/src/manager.rs | 5 +- .../flowy-grid/src/services/grid_editor.rs | 4 +- .../src/services/grid_view_manager.rs | 15 +-- frontend/rust-lib/flowy-revision/Cargo.toml | 1 + .../flowy-revision/src/cache/disk/mod.rs | 47 ++++++- .../rust-lib/flowy-revision/src/cache/mod.rs | 1 + .../flowy-revision/src/cache/reset.rs | 115 ++++++++++++++++++ .../flowy-revision/src/rev_persistence.rs | 10 +- frontend/rust-lib/flowy-sdk/src/lib.rs | 1 + frontend/rust-lib/lib-dispatch/src/data.rs | 2 +- .../src/revision/view_rev.rs | 2 +- .../src/revision/grid_rev.rs | 6 +- .../src/client_folder/folder_pad.rs | 9 +- .../src/client_grid/grid_builder.rs | 10 +- 17 files changed, 259 insertions(+), 55 deletions(-) create mode 100644 frontend/rust-lib/flowy-revision/src/cache/reset.rs diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index fba225a3a3..7534492ab0 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1039,6 +1039,7 @@ dependencies = [ "lib-ot", "lib-ws", "serde", + "serde_json", "strum", "strum_macros", "tokio", diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs index d67d062d25..b8be0b1e19 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs @@ -6,14 +6,20 @@ use crate::{ use flowy_database::kv::KV; use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder_data_model::revision::{AppRevision, ViewRevision, WorkspaceRevision}; +use flowy_folder_data_model::revision::{AppRevision, FolderRevision, ViewRevision, WorkspaceRevision}; use flowy_revision::disk::SQLiteTextBlockRevisionPersistence; -use flowy_revision::{RevisionLoader, RevisionPersistence}; +use flowy_revision::reset::{RevisionResettable, RevisionStructReset}; + +use flowy_sync::client_folder::make_folder_rev_json_str; +use flowy_sync::entities::revision::Revision; use flowy_sync::{client_folder::FolderPad, entities::revision::md5}; + use std::sync::Arc; const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION"; const V2_MIGRATION: &str = "FOLDER_V2_MIGRATION"; +#[allow(dead_code)] +const V3_MIGRATION: &str = "FOLDER_V3_MIGRATION"; pub(crate) struct FolderMigration { user_id: String, @@ -79,32 +85,58 @@ impl FolderMigration { Ok(Some(folder)) } - pub async fn run_v2_migration(&self, user_id: &str, folder_id: &FolderId) -> FlowyResult> { + pub async fn run_v2_migration(&self, folder_id: &FolderId) -> FlowyResult<()> { let key = md5(format!("{}{}", self.user_id, V2_MIGRATION)); if KV::get_bool(&key) { - return Ok(None); + return Ok(()); } - let pool = self.database.db_pool()?; - let disk_cache = SQLiteTextBlockRevisionPersistence::new(user_id, pool); - let rev_persistence = Arc::new(RevisionPersistence::new(user_id, folder_id.as_ref(), disk_cache)); - let (revisions, _) = RevisionLoader { - object_id: folder_id.as_ref().to_owned(), - user_id: self.user_id.clone(), - cloud: None, - rev_persistence, - } - .load() - .await?; - - if revisions.is_empty() { - tracing::trace!("Run folder v2 migration, but revision is empty"); - KV::set_bool(&key, true); - return Ok(None); - } - - let pad = FolderPad::from_revisions(revisions)?; + let _ = self.migration_folder_rev_struct_if_need(folder_id).await?; KV::set_bool(&key, true); tracing::trace!("Run folder v2 migration"); - Ok(Some(pad)) + Ok(()) + } + #[allow(dead_code)] + pub async fn run_v3_migration(&self, folder_id: &FolderId) -> FlowyResult<()> { + let key = md5(format!("{}{}", self.user_id, V3_MIGRATION)); + if KV::get_bool(&key) { + return Ok(()); + } + let _ = self.migration_folder_rev_struct_if_need(folder_id).await?; + KV::set_bool(&key, true); + tracing::trace!("Run folder v3 migration"); + Ok(()) + } + + pub async fn migration_folder_rev_struct_if_need(&self, folder_id: &FolderId) -> FlowyResult<()> { + let object = FolderRevisionResettable { + folder_id: folder_id.as_ref().to_owned(), + }; + + let pool = self.database.db_pool()?; + let disk_cache = SQLiteTextBlockRevisionPersistence::new(&self.user_id, pool); + let reset = RevisionStructReset::new(&self.user_id, object, Arc::new(disk_cache)); + reset.run().await + } +} + +pub struct FolderRevisionResettable { + folder_id: String, +} + +impl RevisionResettable for FolderRevisionResettable { + fn target_id(&self) -> &str { + &self.folder_id + } + + fn target_reset_rev_str(&self, revisions: Vec) -> FlowyResult { + let pad = FolderPad::from_revisions(revisions)?; + let json = pad.to_json()?; + Ok(json) + } + + fn default_target_rev_str(&self) -> FlowyResult { + let folder = FolderRevision::default(); + let json = make_folder_rev_json_str(&folder)?; + Ok(json) } } diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs index ffa5219aa8..dcd28d1906 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs @@ -100,10 +100,9 @@ impl FolderPersistence { self.save_folder(user_id, folder_id, migrated_folder).await?; } - if let Some(migrated_folder) = migrations.run_v2_migration(user_id, folder_id).await? { - self.save_folder(user_id, folder_id, migrated_folder).await?; - } + let _ = migrations.run_v2_migration(folder_id).await?; + // let _ = migrations.run_v3_migration(folder_id).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 4b4ea3b167..048fc58415 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -193,7 +193,7 @@ pub async fn make_grid_view_data( grid_manager: Arc, build_context: BuildGridContext, ) -> FlowyResult { - for block_meta_data in &build_context.blocks_meta_data { + for block_meta_data in &build_context.blocks { let block_id = &block_meta_data.block_id; // Indexing the block's rows block_meta_data.rows.iter().for_each(|row| { @@ -208,6 +208,7 @@ pub async fn make_grid_view_data( let _ = grid_manager.create_grid_block(&block_id, repeated_revision).await?; } + // Will replace the grid_id with the value returned by the gen_grid_id() let grid_id = view_id.to_owned(); let grid_rev = GridRevision::from_build_context(&grid_id, build_context); @@ -219,7 +220,7 @@ pub async fn make_grid_view_data( let _ = grid_manager.create_grid(&grid_id, repeated_revision).await?; // Create grid view - let grid_view = GridViewRevision::new(view_id.to_owned(), view_id.to_owned()); + let grid_view = GridViewRevision::new(grid_id, view_id.to_owned()); let grid_view_delta = make_grid_view_delta(&grid_view); let grid_view_delta_bytes = grid_view_delta.json_bytes(); let repeated_revision: RepeatedRevision = diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index cb35819b55..d1aa57a2a0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -546,8 +546,8 @@ impl GridRevisionEditor { Ok(BuildGridContext { field_revs: duplicated_fields.into_iter().map(Arc::new).collect(), - blocks: duplicated_blocks, - blocks_meta_data, + block_metas: duplicated_blocks, + blocks: blocks_meta_data, }) } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index 70768ef3a4..cdaf72665e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -1,23 +1,18 @@ +use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB}; use crate::manager::GridUser; +use crate::services::block_manager::GridBlockManager; +use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::grid_view_editor::{GridViewRevisionDataSource, GridViewRevisionDelegate, GridViewRevisionEditor}; use bytes::Bytes; - -use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB}; -use crate::services::grid_editor_task::GridServiceTaskScheduler; - -use crate::services::block_manager::GridBlockManager; use dashmap::DashMap; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; use flowy_revision::disk::SQLiteGridViewRevisionPersistence; use flowy_revision::{RevisionCompactor, RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence}; use flowy_sync::client_grid::GridRevisionPad; -use flowy_sync::entities::revision::Revision; - -use flowy_sync::util::make_text_delta_from_revisions; - use flowy_sync::entities::grid::GridSettingChangesetParams; - +use flowy_sync::entities::revision::Revision; +use flowy_sync::util::make_text_delta_from_revisions; use lib_infra::future::{wrap_future, AFFuture}; use std::sync::Arc; use tokio::sync::RwLock; diff --git a/frontend/rust-lib/flowy-revision/Cargo.toml b/frontend/rust-lib/flowy-revision/Cargo.toml index 90257699db..e0fdde3271 100644 --- a/frontend/rust-lib/flowy-revision/Cargo.toml +++ b/frontend/rust-lib/flowy-revision/Cargo.toml @@ -23,6 +23,7 @@ dashmap = "5" serde = { version = "1.0", features = ["derive"] } futures-util = "0.3.15" async-stream = "0.3.2" +serde_json = {version = "1.0"} [features] flowy_unit_test = ["lib-ot/flowy_unit_test"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs index 94c76fab11..501d1e591b 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs @@ -8,9 +8,10 @@ pub use grid_block_impl::*; pub use grid_impl::*; pub use grid_view_impl::*; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use flowy_sync::entities::revision::{RevId, Revision, RevisionRange}; use std::fmt::Debug; +use std::sync::Arc; pub trait RevisionDiskCache: Sync + Send { type Error: Debug; @@ -45,6 +46,50 @@ pub trait RevisionDiskCache: Sync + Send { ) -> Result<(), Self::Error>; } +impl RevisionDiskCache for Arc +where + T: RevisionDiskCache, +{ + type Error = FlowyError; + + fn create_revision_records(&self, revision_records: Vec) -> Result<(), Self::Error> { + (**self).create_revision_records(revision_records) + } + + fn read_revision_records( + &self, + object_id: &str, + rev_ids: Option>, + ) -> Result, Self::Error> { + (**self).read_revision_records(object_id, rev_ids) + } + + fn read_revision_records_with_range( + &self, + object_id: &str, + range: &RevisionRange, + ) -> Result, Self::Error> { + (**self).read_revision_records_with_range(object_id, range) + } + + fn update_revision_record(&self, changesets: Vec) -> FlowyResult<()> { + (**self).update_revision_record(changesets) + } + + fn delete_revision_records(&self, object_id: &str, rev_ids: Option>) -> Result<(), Self::Error> { + (**self).delete_revision_records(object_id, rev_ids) + } + + fn delete_and_insert_records( + &self, + object_id: &str, + deleted_rev_ids: Option>, + inserted_records: Vec, + ) -> Result<(), Self::Error> { + (**self).delete_and_insert_records(object_id, deleted_rev_ids, inserted_records) + } +} + #[derive(Clone, Debug)] pub struct RevisionRecord { pub revision: Revision, diff --git a/frontend/rust-lib/flowy-revision/src/cache/mod.rs b/frontend/rust-lib/flowy-revision/src/cache/mod.rs index 3e592c49b1..4f3ee5c19f 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/mod.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/mod.rs @@ -1,2 +1,3 @@ pub mod disk; pub(crate) mod memory; +pub mod reset; diff --git a/frontend/rust-lib/flowy-revision/src/cache/reset.rs b/frontend/rust-lib/flowy-revision/src/cache/reset.rs new file mode 100644 index 0000000000..cc9228e9ff --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/cache/reset.rs @@ -0,0 +1,115 @@ +use crate::disk::{RevisionDiskCache, RevisionRecord}; +use crate::{RevisionLoader, RevisionPersistence}; +use flowy_database::kv::KV; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sync::entities::revision::Revision; +use lib_ot::core::TextDeltaBuilder; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::sync::Arc; + +pub trait RevisionResettable { + fn target_id(&self) -> &str; + // String in json format + fn target_reset_rev_str(&self, revisions: Vec) -> FlowyResult; + + // String in json format + fn default_target_rev_str(&self) -> FlowyResult; +} + +pub struct RevisionStructReset { + user_id: String, + target: T, + disk_cache: Arc>, +} + +impl RevisionStructReset +where + T: RevisionResettable, +{ + pub fn new(user_id: &str, object: T, disk_cache: Arc>) -> Self { + Self { + user_id: user_id.to_owned(), + target: object, + disk_cache, + } + } + + pub async fn run(&self) -> FlowyResult<()> { + match KV::get_str(self.target.target_id()) { + None => { + tracing::trace!("😁 reset object"); + let _ = self.reset_object().await?; + let _ = self.save_migrate_record()?; + } + Some(s) => { + let mut record = MigrationGridRecord::from_str(&s)?; + let rev_str = self.target.default_target_rev_str()?; + if record.len < rev_str.len() { + let _ = self.reset_object().await?; + record.len = rev_str.len(); + KV::set_str(self.target.target_id(), record.to_string()); + } + } + } + Ok(()) + } + + async fn reset_object(&self) -> FlowyResult<()> { + let rev_persistence = Arc::new(RevisionPersistence::from_disk_cache( + &self.user_id, + self.target.target_id(), + self.disk_cache.clone(), + )); + let (revisions, _) = RevisionLoader { + object_id: self.target.target_id().to_owned(), + user_id: self.user_id.clone(), + cloud: None, + rev_persistence, + } + .load() + .await?; + + let s = self.target.target_reset_rev_str(revisions)?; + let delta_data = TextDeltaBuilder::new().insert(&s).build().json_bytes(); + let revision = Revision::initial_revision(&self.user_id, self.target.target_id(), delta_data); + let record = RevisionRecord::new(revision); + + tracing::trace!("Reset {} revision record object", self.target.target_id()); + let _ = self + .disk_cache + .delete_and_insert_records(self.target.target_id(), None, vec![record]); + + Ok(()) + } + + fn save_migrate_record(&self) -> FlowyResult<()> { + let rev_str = self.target.default_target_rev_str()?; + let record = MigrationGridRecord { + object_id: self.target.target_id().to_owned(), + len: rev_str.len(), + }; + KV::set_str(self.target.target_id(), record.to_string()); + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +struct MigrationGridRecord { + object_id: String, + len: usize, +} + +impl FromStr for MigrationGridRecord { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str::(s) + } +} + +impl ToString for MigrationGridRecord { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "".to_string()) + } +} diff --git a/frontend/rust-lib/flowy-revision/src/rev_persistence.rs b/frontend/rust-lib/flowy-revision/src/rev_persistence.rs index eb3da339b7..01f4a4189d 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_persistence.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_persistence.rs @@ -28,9 +28,17 @@ impl RevisionPersistence { where C: 'static + RevisionDiskCache, { + let disk_cache = Arc::new(disk_cache) as Arc>; + Self::from_disk_cache(user_id, object_id, disk_cache) + } + + pub fn from_disk_cache( + user_id: &str, + object_id: &str, + disk_cache: Arc>, + ) -> RevisionPersistence { let object_id = object_id.to_owned(); let user_id = user_id.to_owned(); - let disk_cache = Arc::new(disk_cache) as Arc>; let sync_seq = RwLock::new(RevisionSyncSequence::new()); let memory_cache = Arc::new(RevisionMemoryCache::new(&object_id, Arc::new(disk_cache.clone()))); Self { diff --git a/frontend/rust-lib/flowy-sdk/src/lib.rs b/frontend/rust-lib/flowy-sdk/src/lib.rs index 00a3785122..602e1d2092 100644 --- a/frontend/rust-lib/flowy-sdk/src/lib.rs +++ b/frontend/rust-lib/flowy-sdk/src/lib.rs @@ -75,6 +75,7 @@ fn crate_log_filter(level: String) -> String { filters.push(format!("lib_ws={}", level)); filters.push(format!("lib_infra={}", level)); filters.push(format!("flowy_sync={}", level)); + filters.push(format!("flowy_revision={}", level)); // filters.push(format!("lib_dispatch={}", level)); filters.push(format!("dart_ffi={}", "info")); diff --git a/frontend/rust-lib/lib-dispatch/src/data.rs b/frontend/rust-lib/lib-dispatch/src/data.rs index 15f9684a16..74d5ab6f17 100644 --- a/frontend/rust-lib/lib-dispatch/src/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/data.rs @@ -57,7 +57,7 @@ where match self.into_inner().into_bytes() { Ok(bytes) => { log::trace!("Serialize Data: {:?} to event response", std::any::type_name::()); - return ResponseBuilder::Ok().data(bytes).build(); + ResponseBuilder::Ok().data(bytes).build() } Err(e) => e.into(), } diff --git a/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs b/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs index e542fb9bbd..84b447a384 100644 --- a/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs +++ b/shared-lib/flowy-folder-data-model/src/revision/view_rev.rs @@ -18,7 +18,7 @@ pub struct ViewRevision { #[serde(default)] pub data_type: ViewDataTypeRevision, - pub version: i64, + pub version: i64, // Deprecated pub belongings: Vec, diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index 31e6265757..f8303c973d 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -38,7 +38,7 @@ impl GridRevision { Self { grid_id: grid_id.to_owned(), fields: context.field_revs, - blocks: context.blocks.into_iter().map(Arc::new).collect(), + blocks: context.block_metas.into_iter().map(Arc::new).collect(), } } } @@ -186,8 +186,8 @@ pub trait TypeOptionDataDeserializer { #[derive(Clone, Default, Deserialize, Serialize)] pub struct BuildGridContext { pub field_revs: Vec>, - pub blocks: Vec, - pub blocks_meta_data: Vec, + pub block_metas: Vec, + pub blocks: Vec, } impl BuildGridContext { diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index 63df0ce828..acef097250 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -319,11 +319,16 @@ impl FolderPad { } pub fn to_json(&self) -> CollaborateResult { - serde_json::to_string(&self.folder_rev) - .map_err(|e| CollaborateError::internal().context(format!("serial trash to json failed: {}", e))) + make_folder_rev_json_str(&self.folder_rev) } } +pub fn make_folder_rev_json_str(folder_rev: &FolderRevision) -> CollaborateResult { + let json = serde_json::to_string(folder_rev) + .map_err(|err| internal_error(format!("Serialize folder to json str failed. {:?}", err)))?; + Ok(json) +} + impl FolderPad { fn modify_workspaces(&mut self, f: F) -> CollaborateResult> where diff --git a/shared-lib/flowy-sync/src/client_grid/grid_builder.rs b/shared-lib/flowy-sync/src/client_grid/grid_builder.rs index ea5d5a9332..ad3f443505 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_builder.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_builder.rs @@ -18,8 +18,8 @@ impl std::default::Default for GridBuilder { rows: vec![], }; - build_context.blocks.push(block_meta); - build_context.blocks_meta_data.push(block_meta_data); + build_context.block_metas.push(block_meta); + build_context.blocks.push(block_meta_data); GridBuilder { build_context } } @@ -34,8 +34,8 @@ impl GridBuilder { } pub fn add_row(&mut self, row_rev: RowRevision) { - let block_meta_rev = self.build_context.blocks.first_mut().unwrap(); - let block_rev = self.build_context.blocks_meta_data.first_mut().unwrap(); + let block_meta_rev = self.build_context.block_metas.first_mut().unwrap(); + let block_rev = self.build_context.blocks.first_mut().unwrap(); block_rev.rows.push(Arc::new(row_rev)); block_meta_rev.row_count += 1; } @@ -50,7 +50,7 @@ impl GridBuilder { } pub fn block_id(&self) -> &str { - &self.build_context.blocks.first().unwrap().block_id + &self.build_context.block_metas.first().unwrap().block_id } pub fn build(self) -> BuildGridContext { From ad26f9c86d2e0edd8fc2ee5f69929b0baf580869 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 11:31:43 +0800 Subject: [PATCH 155/224] =?UTF-8?q?fix:=20checkbox=20+=20underline=20doesn?= =?UTF-8?q?=E2=80=99t=20work=20when=20the=20checkbox=20is=20checked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/render/rich_text/checkbox_text.dart | 8 +- .../flowy_editor/test/infra/test_editor.dart | 6 +- .../render/rich_text/checkbox_text_test.dart | 73 +++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/test/render/rich_text/checkbox_text_test.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart index c7ba8607a6..bfcc938b8b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -72,8 +72,8 @@ class _CheckboxNodeWidgetState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( + key: iconKey, child: FlowySvg( - key: iconKey, size: Size.square(_iconSize), padding: EdgeInsets.only( top: topPadding, right: _iconRightPadding), @@ -149,7 +149,11 @@ class _CheckboxNodeWidgetState extends State style: widget.textNode.attributes.check ? span.style?.copyWith( color: Colors.grey, - decoration: TextDecoration.lineThrough, + decoration: TextDecoration.combine([ + TextDecoration.lineThrough, + if (span.style?.decoration != null) + span.style!.decoration! + ]), ) : span.style, recognizer: span.recognizer, diff --git a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart index 61ece83c5a..17b0a95318 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/infra/test_editor.dart @@ -47,13 +47,11 @@ class EditorWidgetTester { insert(TextNode.empty()); } - void insertTextNode(String? text, {Attributes? attributes}) { + void insertTextNode(String? text, {Attributes? attributes, Delta? delta}) { insert( TextNode( type: 'text', - delta: Delta( - [TextInsert(text ?? 'Test')], - ), + delta: delta ?? Delta([TextInsert(text ?? 'Test')]), attributes: attributes, ), ); diff --git a/frontend/app_flowy/packages/flowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/app_flowy/packages/flowy_editor/test/render/rich_text/checkbox_text_test.dart new file mode 100644 index 0000000000..84f6b93990 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -0,0 +1,73 @@ +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/src/render/rich_text/default_selectable.dart'; +import 'package:flowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('delete_text_handler.dart', () { + testWidgets('Presses backspace key in empty document', (tester) async { + // Before + // + // [BIUS]Welcome to Appflowy 😁[BIUS] + // + // After + // + // [checkbox]Welcome to Appflowy 😁 + // + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode( + '', + attributes: { + StyleKey.subtype: StyleKey.checkbox, + StyleKey.checkbox: false, + }, + delta: Delta([ + TextInsert(text, { + StyleKey.bold: true, + StyleKey.italic: true, + StyleKey.underline: true, + StyleKey.strikethrough: true, + }), + ]), + ); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + + final selection = + Selection.single(path: [0], startOffset: 0, endOffset: text.length); + var node = editor.nodeAtPath([0]) as TextNode; + var state = node.key?.currentState as DefaultSelectable; + var checkboxWidget = find.byKey(state.iconKey!); + await tester.tap(checkboxWidget); + await tester.pumpAndSettle(); + + expect(node.attributes.check, true); + + expect(node.allSatisfyBoldInSelection(selection), true); + expect(node.allSatisfyItalicInSelection(selection), true); + expect(node.allSatisfyUnderlineInSelection(selection), true); + expect(node.allSatisfyStrikethroughInSelection(selection), true); + + node = editor.nodeAtPath([0]) as TextNode; + state = node.key?.currentState as DefaultSelectable; + await tester.ensureVisible(find.byKey(state.iconKey!)); + await tester.tap(find.byKey(state.iconKey!)); + await tester.pump(); + + expect(node.attributes.check, false); + expect(node.allSatisfyBoldInSelection(selection), true); + expect(node.allSatisfyItalicInSelection(selection), true); + expect(node.allSatisfyUnderlineInSelection(selection), true); + expect(node.allSatisfyStrikethroughInSelection(selection), true); + }); + }); +} From 03a70bbdb953a46bb771e1f6962125ad6b969d58 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 11:37:34 +0800 Subject: [PATCH 156/224] chore: update grid migration --- .../src/services/persistence/migration.rs | 20 +-- frontend/rust-lib/flowy-grid/src/manager.rs | 2 +- .../src/services/persistence/migration.rs | 114 ++++++------------ 3 files changed, 49 insertions(+), 87 deletions(-) diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs index b8be0b1e19..7b211fb54a 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs @@ -5,15 +5,12 @@ use crate::{ }; use flowy_database::kv::KV; use flowy_error::{FlowyError, FlowyResult}; - use flowy_folder_data_model::revision::{AppRevision, FolderRevision, ViewRevision, WorkspaceRevision}; use flowy_revision::disk::SQLiteTextBlockRevisionPersistence; use flowy_revision::reset::{RevisionResettable, RevisionStructReset}; - use flowy_sync::client_folder::make_folder_rev_json_str; use flowy_sync::entities::revision::Revision; use flowy_sync::{client_folder::FolderPad, entities::revision::md5}; - use std::sync::Arc; const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION"; @@ -35,7 +32,7 @@ impl FolderMigration { } pub fn run_v1_migration(&self) -> FlowyResult> { - let key = md5(format!("{}{}", self.user_id, V1_MIGRATION)); + let key = migration_flag_key(&self.user_id, V1_MIGRATION); if KV::get_bool(&key) { return Ok(None); } @@ -86,28 +83,29 @@ impl FolderMigration { } pub async fn run_v2_migration(&self, folder_id: &FolderId) -> FlowyResult<()> { - let key = md5(format!("{}{}", self.user_id, V2_MIGRATION)); + let key = migration_flag_key(&self.user_id, V2_MIGRATION); if KV::get_bool(&key) { return Ok(()); } - let _ = self.migration_folder_rev_struct_if_need(folder_id).await?; + let _ = self.migration_folder_rev_struct(folder_id).await?; KV::set_bool(&key, true); tracing::trace!("Run folder v2 migration"); Ok(()) } + #[allow(dead_code)] pub async fn run_v3_migration(&self, folder_id: &FolderId) -> FlowyResult<()> { - let key = md5(format!("{}{}", self.user_id, V3_MIGRATION)); + let key = migration_flag_key(&self.user_id, V3_MIGRATION); if KV::get_bool(&key) { return Ok(()); } - let _ = self.migration_folder_rev_struct_if_need(folder_id).await?; + let _ = self.migration_folder_rev_struct(folder_id).await?; KV::set_bool(&key, true); tracing::trace!("Run folder v3 migration"); Ok(()) } - pub async fn migration_folder_rev_struct_if_need(&self, folder_id: &FolderId) -> FlowyResult<()> { + pub async fn migration_folder_rev_struct(&self, folder_id: &FolderId) -> FlowyResult<()> { let object = FolderRevisionResettable { folder_id: folder_id.as_ref().to_owned(), }; @@ -119,6 +117,10 @@ impl FolderMigration { } } +fn migration_flag_key(user_id: &str, version: &str) -> String { + md5(format!("{}{}", user_id, version,)) +} + pub struct FolderRevisionResettable { folder_id: String, } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 048fc58415..9e4556b793 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -96,7 +96,7 @@ impl GridManager { pub async fn open_grid>(&self, grid_id: T) -> FlowyResult> { let grid_id = grid_id.as_ref(); tracing::Span::current().record("grid_id", &grid_id); - let _ = self.migration.migration_grid_if_need(grid_id).await; + let _ = self.migration.run_v1_migration(grid_id).await; self.get_or_create_grid_editor(grid_id).await } diff --git a/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs index bf3aa7adfb..cb99d8eb6d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs +++ b/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs @@ -1,19 +1,17 @@ use crate::manager::GridUser; - use crate::services::persistence::GridDatabase; use flowy_database::kv::KV; use flowy_error::FlowyResult; use flowy_grid_data_model::revision::GridRevision; -use flowy_revision::disk::{RevisionRecord, SQLiteGridRevisionPersistence}; -use flowy_revision::{mk_grid_block_revision_disk_cache, RevisionLoader, RevisionPersistence}; +use flowy_revision::disk::SQLiteGridRevisionPersistence; +use flowy_revision::reset::{RevisionResettable, RevisionStructReset}; use flowy_sync::client_grid::{make_grid_rev_json_str, GridRevisionPad}; use flowy_sync::entities::revision::Revision; - -use lib_ot::core::TextDeltaBuilder; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; +use flowy_sync::util::md5; use std::sync::Arc; +const V1_MIGRATION: &str = "GRID_V1_MIGRATION"; + pub(crate) struct GridMigration { user: Arc, database: Arc, @@ -24,90 +22,52 @@ impl GridMigration { Self { user, database } } - pub async fn migration_grid_if_need(&self, grid_id: &str) -> FlowyResult<()> { - match KV::get_str(grid_id) { - None => { - let _ = self.reset_grid_rev(grid_id).await?; - let _ = self.save_migrate_record(grid_id)?; - } - Some(s) => { - let mut record = MigrationGridRecord::from_str(&s)?; - let empty_json = self.empty_grid_rev_json()?; - if record.len < empty_json.len() { - let _ = self.reset_grid_rev(grid_id).await?; - record.len = empty_json.len(); - KV::set_str(grid_id, record.to_string()); - } - } - } - Ok(()) - } - - async fn reset_grid_rev(&self, grid_id: &str) -> FlowyResult<()> { + pub async fn run_v1_migration(&self, grid_id: &str) -> FlowyResult<()> { let user_id = self.user.user_id()?; - let pool = self.database.db_pool()?; - let grid_rev_pad = self.get_grid_revision_pad(grid_id).await?; - let json = grid_rev_pad.json_str()?; - let delta_data = TextDeltaBuilder::new().insert(&json).build().json_bytes(); - let revision = Revision::initial_revision(&user_id, grid_id, delta_data); - let record = RevisionRecord::new(revision); - // - let disk_cache = mk_grid_block_revision_disk_cache(&user_id, pool); - let _ = disk_cache.delete_and_insert_records(grid_id, None, vec![record]); + let key = migration_flag_key(&user_id, V1_MIGRATION, grid_id); + if KV::get_bool(&key) { + return Ok(()); + } + let _ = self.migration_grid_rev_struct(grid_id).await?; + tracing::trace!("Run grid:{} v1 migration", grid_id); + KV::set_bool(&key, true); Ok(()) } - fn save_migrate_record(&self, grid_id: &str) -> FlowyResult<()> { - let empty_json_str = self.empty_grid_rev_json()?; - let record = MigrationGridRecord { + pub async fn migration_grid_rev_struct(&self, grid_id: &str) -> FlowyResult<()> { + let object = GridRevisionResettable { grid_id: grid_id.to_owned(), - len: empty_json_str.len(), }; - KV::set_str(grid_id, record.to_string()); - Ok(()) - } - - fn empty_grid_rev_json(&self) -> FlowyResult { - let empty_grid_rev = GridRevision::default(); - let empty_json = make_grid_rev_json_str(&empty_grid_rev)?; - Ok(empty_json) - } - - async fn get_grid_revision_pad(&self, grid_id: &str) -> FlowyResult { - let pool = self.database.db_pool()?; let user_id = self.user.user_id()?; + let pool = self.database.db_pool()?; let disk_cache = SQLiteGridRevisionPersistence::new(&user_id, pool); - let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, grid_id, disk_cache)); - let (revisions, _) = RevisionLoader { - object_id: grid_id.to_owned(), - user_id, - cloud: None, - rev_persistence, - } - .load() - .await?; - - let pad = GridRevisionPad::from_revisions(revisions)?; - Ok(pad) + let reset = RevisionStructReset::new(&user_id, object, Arc::new(disk_cache)); + reset.run().await } } -#[derive(Serialize, Deserialize)] -struct MigrationGridRecord { +fn migration_flag_key(user_id: &str, version: &str, grid_id: &str) -> String { + md5(format!("{}{}{}", user_id, version, grid_id,)) +} + +pub struct GridRevisionResettable { grid_id: String, - len: usize, } -impl FromStr for MigrationGridRecord { - type Err = serde_json::Error; +impl RevisionResettable for GridRevisionResettable { + fn target_id(&self) -> &str { + &self.grid_id + } - fn from_str(s: &str) -> Result { - serde_json::from_str::(s) - } -} - -impl ToString for MigrationGridRecord { - fn to_string(&self) -> String { - serde_json::to_string(self).unwrap_or_else(|_| "".to_string()) + fn target_reset_rev_str(&self, revisions: Vec) -> FlowyResult { + let pad = GridRevisionPad::from_revisions(revisions)?; + let json = pad.json_str()?; + Ok(json) + } + + fn default_target_rev_str(&self) -> FlowyResult { + let grid_rev = GridRevision::default(); + let json = make_grid_rev_json_str(&grid_rev)?; + Ok(json) } } From 6a527a6676284159820566772afdce22a3c36474 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 15:08:51 +0800 Subject: [PATCH 157/224] docs: readme --- .../app_flowy/packages/flowy_editor/README.md | 58 +- .../packages/flowy_editor/coverage/lcov.info | 3458 +++++++++++++++++ .../documentation/contributing.md | 14 + .../flowy_editor/documentation/testing.md | 129 + .../flowy_editor/example/lib/main.dart | 40 +- .../lib/src/document/state_tree.dart | 13 + .../flowy_editor/lib/src/editor_state.dart | 4 + 7 files changed, 3678 insertions(+), 38 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/coverage/lcov.info create mode 100644 frontend/app_flowy/packages/flowy_editor/documentation/contributing.md create mode 100644 frontend/app_flowy/packages/flowy_editor/documentation/testing.md diff --git a/frontend/app_flowy/packages/flowy_editor/README.md b/frontend/app_flowy/packages/flowy_editor/README.md index 8b55e735b5..bc41772576 100644 --- a/frontend/app_flowy/packages/flowy_editor/README.md +++ b/frontend/app_flowy/packages/flowy_editor/README.md @@ -11,15 +11,30 @@ and the Flutter guide for [developing packages and plugins](https://flutter.dev/developing-packages). --> -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +一个可扩展,测试覆盖的 flutter 富文本编辑组件 ## Features TODO: List what your package can do. Maybe include images, gifs, or videos. +* 可扩展的 + * 支持扩展不同样式的视图 + * 支持定制快捷键解析 + * 支持扩展toolbar/popup list样式(WIP) + * ... +* 协同结构 ready + * +* 质量保证的 + * 由于可扩展的结构,以及随着功能的增多,我们鼓励每个提交的文件或者代码段,都可以在test下增加对应的测试用例代码,尽可能得保证提交者不需要担心自己的代码影响了已有的逻辑。 + + ## Getting started +```shell +flutter pub add flowy_editor +flutter pub get +``` + TODO: List prerequisites and provide or point to information on how to start using the package. @@ -28,12 +43,47 @@ start using the package. TODO: Include short and useful examples for package users. Add longer examples to `/example` folder. +Empty document ```dart -const like = 'sample'; +final editorState = EditorState.empty(); +final editor = FlowyEditor( + editorState: editorState, + keyEventHandlers: const [], + customBuilders: const {}, +); ``` -## Additional information +从JSON文件中读取 +```dart +final json = ...; +final editorState = EditorState(StateTree.fromJson(data)); +final editor = FlowyEditor( + editorState: editorState, + keyEventHandlers: const [], + customBuilders: const {}, +); +``` +For more. Run the example. +```shell +git clone https://github.com/AppFlowy-IO/AppFlowy.git +cd frontend/app_flowy/packages/flowy_editor/example +flutter run +``` + +## Examples + +## Documentation +* 术语表 + +## Additional information TODO: Tell users more about the package: where to find more information, how to contribute to the package, how to file issues, what response they can expect from the package authors, and more. + +目前正在完善更多的文档信息 +* Selection +* + +我们还有很多工作需要继续完成, +Project checker link. diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info new file mode 100644 index 0000000000..448f8e89a3 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info @@ -0,0 +1,3458 @@ +SF:lib/src/document/node.dart +DA:17,9 +DA:19,18 +DA:20,12 +DA:22,8 +DA:27,20 +DA:29,14 +DA:31,11 +DA:37,13 +DA:38,2 +DA:42,1 +DA:43,3 +DA:46,1 +DA:47,1 +DA:48,1 +DA:49,2 +DA:50,2 +DA:52,1 +DA:54,1 +DA:55,1 +DA:56,2 +DA:57,1 +DA:65,1 +DA:66,1 +DA:67,2 +DA:68,1 +DA:74,1 +DA:81,2 +DA:82,1 +DA:88,5 +DA:89,20 +DA:91,17 +DA:95,10 +DA:98,11 +DA:99,33 +DA:103,22 +DA:106,11 +DA:107,11 +DA:111,44 +DA:114,9 +DA:115,18 +DA:117,18 +DA:118,9 +DA:119,18 +DA:120,9 +DA:124,16 +DA:126,8 +DA:127,24 +DA:128,0 +DA:129,0 +DA:131,0 +DA:135,9 +DA:137,18 +DA:138,9 +DA:141,18 +DA:144,0 +DA:146,0 +DA:147,0 +DA:150,0 +DA:153,4 +DA:155,4 +DA:157,8 +DA:158,4 +DA:161,2 +DA:162,2 +DA:163,2 +DA:165,4 +DA:166,5 +DA:168,4 +DA:169,2 +DA:174,10 +DA:175,10 +DA:179,30 +DA:180,10 +DA:183,9 +DA:185,40 +DA:188,1 +DA:189,1 +DA:190,4 +DA:192,1 +DA:193,0 +DA:194,0 +DA:195,0 +DA:204,10 +DA:210,27 +DA:212,2 +DA:213,6 +DA:214,2 +DA:216,2 +DA:217,2 +DA:220,9 +DA:221,9 +DA:224,5 +DA:225,5 +DA:226,5 +DA:229,1 +DA:231,1 +DA:232,3 +DA:236,1 +DA:242,1 +DA:243,1 +DA:244,1 +DA:245,0 +DA:246,0 +DA:249,3 +DA:251,3 +DA:252,3 +DA:253,3 +DA:254,6 +DA:255,6 +DA:257,3 +DA:258,0 +DA:259,0 +DA:260,0 +DA:265,27 +LF:114 +LH:99 +end_of_record +SF:lib/src/document/state_tree.dart +DA:11,11 +DA:15,1 +DA:16,3 +DA:18,2 +DA:19,1 +DA:20,1 +DA:23,7 +DA:24,14 +DA:27,3 +DA:28,3 +DA:31,6 +DA:32,23 +DA:35,0 +DA:36,0 +DA:39,0 +DA:40,0 +DA:46,9 +DA:47,3 +DA:48,3 +DA:54,5 +DA:55,5 +DA:58,10 +DA:59,5 +DA:62,15 +DA:66,4 +DA:67,4 +DA:70,8 +DA:71,4 +DA:72,4 +DA:73,4 +DA:74,4 +DA:79,5 +DA:80,5 +DA:83,10 +DA:87,5 +LF:35 +LH:31 +end_of_record +SF:lib/src/document/path.dart +DA:7,10 +DA:8,10 +LF:2 +LH:2 +end_of_record +SF:lib/src/document/position.dart +DA:9,10 +DA:14,10 +DA:16,10 +DA:19,60 +DA:22,1 +DA:24,2 +DA:25,2 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:35,9 +DA:36,27 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +LF:17 +LH:9 +end_of_record +SF:lib/src/document/selection.dart +DA:14,6 +DA:25,7 +DA:29,7 +DA:30,7 +DA:33,6 +DA:40,40 +DA:41,54 +DA:42,8 +DA:43,80 +DA:44,48 +DA:45,6 +DA:46,60 +DA:47,20 +DA:49,0 +DA:50,0 +DA:51,0 +DA:56,12 +DA:58,1 +DA:60,3 +DA:62,3 +DA:66,5 +DA:67,5 +DA:68,5 +DA:69,5 +DA:73,20 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:82,9 +DA:84,9 +DA:90,18 +DA:93,0 +DA:94,0 +DA:96,9 +DA:97,27 +LF:36 +LH:27 +end_of_record +SF:lib/src/document/text_delta.dart +DA:13,21 +DA:17,4 +DA:26,10 +DA:28,9 +DA:30,18 +DA:33,10 +DA:35,10 +DA:38,5 +DA:40,5 +DA:43,15 +DA:44,15 +DA:47,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:55,1 +DA:57,1 +DA:58,1 +DA:60,1 +DA:62,3 +DA:72,7 +DA:76,7 +DA:78,14 +DA:81,7 +DA:83,7 +DA:86,0 +DA:87,0 +DA:90,6 +DA:92,6 +DA:95,1 +DA:97,1 +DA:100,6 +DA:103,0 +DA:105,0 +DA:106,0 +DA:109,1 +DA:111,1 +DA:112,1 +DA:114,1 +DA:116,3 +DA:125,6 +DA:127,6 +DA:129,6 +DA:132,1 +DA:133,1 +DA:136,1 +DA:138,1 +DA:141,3 +DA:144,0 +DA:146,0 +DA:149,1 +DA:151,1 +DA:152,1 +DA:162,7 +DA:163,7 +DA:165,6 +DA:166,12 +DA:169,6 +DA:170,24 +DA:174,18 +DA:177,6 +DA:178,24 +DA:179,18 +DA:180,18 +DA:185,6 +DA:188,24 +DA:189,4 +DA:192,18 +DA:194,6 +DA:195,6 +DA:196,12 +DA:197,6 +DA:198,12 +DA:199,6 +DA:201,10 +DA:203,6 +DA:204,4 +DA:207,6 +DA:208,5 +DA:210,5 +DA:214,6 +DA:215,6 +DA:216,18 +DA:217,6 +DA:221,0 +DA:224,5 +DA:225,5 +DA:226,5 +DA:227,4 +DA:228,3 +DA:230,2 +DA:231,2 +DA:232,2 +DA:233,6 +DA:234,2 +DA:235,2 +DA:236,4 +DA:241,1 +DA:244,2 +DA:245,1 +DA:247,2 +DA:248,2 +DA:249,1 +DA:251,2 +DA:252,2 +DA:253,2 +DA:270,2 +DA:271,2 +DA:273,3 +DA:274,1 +DA:276,1 +DA:280,2 +DA:283,19 +DA:285,2 +DA:286,4 +DA:289,7 +DA:290,7 +DA:293,6 +DA:295,12 +DA:296,10 +DA:297,7 +DA:298,3 +DA:301,15 +DA:302,10 +DA:303,12 +DA:308,5 +DA:309,4 +DA:310,4 +DA:311,4 +DA:314,6 +DA:315,0 +DA:321,12 +DA:327,7 +DA:328,7 +DA:329,14 +DA:332,13 +DA:334,6 +DA:335,10 +DA:337,11 +DA:338,6 +DA:341,12 +DA:349,3 +DA:350,6 +DA:357,7 +DA:358,14 +DA:361,18 +DA:364,5 +DA:365,10 +DA:366,15 +DA:370,6 +DA:371,12 +DA:372,12 +DA:373,6 +DA:375,6 +DA:377,6 +DA:378,5 +DA:379,5 +DA:381,20 +DA:382,8 +DA:383,4 +DA:384,4 +DA:386,15 +DA:387,12 +DA:391,6 +DA:392,12 +DA:393,10 +DA:394,3 +DA:395,3 +DA:396,10 +DA:397,1 +DA:398,1 +DA:401,15 +DA:402,5 +DA:403,5 +DA:404,5 +DA:405,15 +DA:406,15 +DA:408,5 +DA:409,1 +DA:410,5 +DA:411,10 +DA:415,5 +DA:419,5 +DA:420,10 +DA:421,15 +DA:422,10 +DA:423,10 +DA:425,8 +DA:426,2 +DA:431,6 +DA:435,5 +DA:436,10 +DA:437,10 +DA:438,6 +DA:439,6 +DA:441,5 +DA:444,7 +DA:445,14 +DA:448,6 +DA:449,12 +DA:450,16 +DA:451,6 +DA:455,1 +DA:457,1 +DA:460,3 +DA:463,0 +DA:465,0 +DA:469,7 +DA:470,7 +DA:471,20 +DA:472,6 +DA:473,6 +DA:474,11 +DA:475,10 +DA:476,10 +DA:477,7 +DA:478,5 +DA:479,10 +DA:480,10 +DA:481,5 +DA:482,4 +DA:483,4 +DA:484,4 +DA:485,6 +DA:488,5 +DA:492,7 +DA:495,2 +DA:496,8 +DA:506,2 +DA:507,2 +DA:508,2 +DA:510,2 +DA:511,6 +DA:512,6 +DA:513,6 +DA:523,2 +DA:524,2 +DA:525,6 +DA:526,1 +DA:528,6 +DA:530,10 +DA:531,6 +DA:532,4 +DA:536,2 +DA:539,10 +DA:540,10 +DA:541,60 +DA:542,10 +DA:545,10 +DA:546,20 +DA:549,2 +DA:550,4 +DA:551,4 +DA:553,2 +DA:554,6 +DA:555,8 +LF:257 +LH:241 +end_of_record +SF:lib/src/document/attributes.dart +DA:3,0 +DA:4,0 +DA:5,0 +DA:8,6 +DA:9,0 +DA:10,2 +DA:11,23 +DA:12,20 +DA:13,10 +DA:17,18 +DA:18,24 +DA:19,3 +DA:25,7 +DA:27,5 +DA:28,5 +DA:29,14 +DA:32,21 +DA:35,14 +DA:36,14 +DA:37,15 +DA:41,7 +LF:21 +LH:17 +end_of_record +SF:lib/src/editor_state.dart +DA:17,24 +DA:23,12 +DA:57,10 +DA:58,10 +DA:61,9 +DA:64,9 +DA:65,24 +DA:67,9 +DA:72,10 +DA:75,20 +DA:82,7 +DA:85,13 +DA:86,6 +DA:89,21 +DA:90,14 +DA:93,7 +DA:94,14 +DA:95,14 +DA:96,7 +DA:97,7 +DA:98,14 +DA:100,14 +DA:101,7 +DA:102,1 +DA:103,1 +DA:104,2 +DA:105,2 +DA:106,2 +DA:107,3 +DA:111,7 +DA:112,7 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:126,6 +DA:127,6 +DA:128,8 +DA:129,6 +DA:130,16 +DA:131,5 +DA:132,15 +DA:133,5 +DA:134,20 +LF:47 +LH:40 +end_of_record +SF:lib/src/operation/operation.dart +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:17,0 +DA:20,8 +DA:29,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:36,6 +DA:38,1 +DA:39,2 +DA:41,1 +DA:42,1 +DA:44,1 +DA:46,1 +DA:47,1 +DA:48,1 +DA:52,1 +DA:54,1 +DA:56,2 +DA:57,4 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:73,4 +DA:77,4 +DA:79,0 +DA:81,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:87,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:96,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:110,0 +DA:111,0 +DA:113,0 +DA:114,0 +DA:117,4 +DA:120,4 +DA:122,3 +DA:123,6 +DA:125,3 +DA:126,3 +DA:128,1 +DA:130,3 +DA:133,1 +DA:135,1 +DA:137,2 +DA:138,4 +DA:147,0 +DA:148,0 +DA:149,0 +DA:150,0 +DA:151,0 +DA:154,6 +DA:158,6 +DA:160,3 +DA:161,3 +DA:162,6 +DA:164,3 +DA:165,3 +DA:167,1 +DA:169,4 +DA:172,0 +DA:174,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:183,4 +DA:184,12 +DA:187,8 +DA:191,13 +DA:192,3 +DA:196,12 +DA:197,8 +DA:198,4 +DA:199,12 +DA:200,4 +DA:201,6 +DA:203,4 +DA:205,4 +DA:209,6 +DA:210,6 +DA:211,6 +DA:212,2 +DA:213,6 +DA:214,16 +DA:215,4 +LF:106 +LH:57 +end_of_record +SF:lib/src/operation/transaction.dart +DA:21,8 +DA:27,1 +DA:28,1 +DA:29,4 +DA:31,1 +DA:32,0 +DA:34,1 +DA:35,0 +LF:8 +LH:6 +end_of_record +SF:lib/src/operation/transaction_builder.dart +DA:23,8 +DA:26,7 +DA:27,7 +DA:28,14 +DA:32,2 +DA:33,4 +DA:37,2 +DA:38,6 +DA:39,12 +DA:43,4 +DA:44,12 +DA:46,8 +DA:47,8 +DA:48,4 +DA:49,8 +DA:55,4 +DA:56,8 +DA:59,3 +DA:60,6 +DA:66,4 +DA:67,4 +DA:70,4 +DA:71,12 +DA:72,4 +DA:73,8 +DA:74,24 +DA:75,4 +DA:78,24 +DA:81,6 +DA:82,18 +DA:83,6 +DA:85,6 +DA:87,12 +DA:89,12 +DA:92,0 +DA:93,0 +DA:96,2 +DA:98,4 +DA:99,4 +DA:100,2 +DA:102,4 +DA:103,2 +DA:104,4 +DA:105,6 +DA:107,4 +DA:108,2 +DA:109,2 +DA:118,1 +DA:121,1 +DA:123,0 +DA:125,1 +DA:127,2 +DA:128,1 +DA:129,1 +DA:134,2 +DA:135,4 +DA:139,1 +DA:140,1 +DA:142,2 +DA:143,1 +DA:144,1 +DA:145,2 +DA:149,3 +DA:150,3 +DA:152,6 +DA:153,3 +DA:154,3 +DA:155,3 +DA:156,9 +DA:159,1 +DA:163,3 +DA:164,1 +DA:165,0 +DA:168,1 +DA:170,2 +DA:171,1 +DA:172,1 +DA:173,1 +DA:175,2 +DA:176,1 +DA:177,1 +DA:178,2 +DA:188,8 +DA:189,28 +DA:191,6 +DA:192,4 +DA:193,3 +DA:194,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:199,0 +DA:203,30 +DA:204,18 +DA:206,20 +DA:209,14 +DA:213,8 +DA:214,8 +DA:215,16 +DA:216,8 +DA:217,8 +LF:101 +LH:92 +end_of_record +SF:lib/src/render/selection/selectable.dart +DA:24,0 +DA:28,0 +DA:43,0 +LF:3 +LH:0 +end_of_record +SF:lib/src/service/editor_service.dart +DA:19,27 +DA:20,9 +DA:21,9 +DA:22,9 +DA:23,9 +DA:24,9 +DA:25,9 +DA:26,9 +DA:30,9 +DA:35,9 +DA:45,9 +DA:46,9 +DA:50,27 +DA:52,9 +DA:54,9 +DA:56,36 +DA:59,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:68,9 +DA:70,9 +DA:71,27 +DA:72,9 +DA:73,27 +DA:74,9 +DA:75,9 +DA:76,27 +DA:77,9 +DA:78,9 +DA:79,27 +DA:80,9 +DA:81,9 +DA:82,18 +DA:84,9 +DA:85,9 +DA:86,27 +DA:87,9 +DA:89,36 +DA:90,9 +DA:92,27 +DA:93,9 +DA:102,18 +DA:103,9 +DA:104,9 +DA:105,9 +DA:106,18 +LF:47 +LH:43 +end_of_record +SF:lib/src/service/render_plugin_service.dart +DA:39,9 +DA:45,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:59,9 +DA:63,9 +DA:69,9 +DA:71,9 +DA:73,30 +DA:74,18 +DA:75,18 +DA:76,9 +DA:77,9 +DA:78,9 +DA:80,0 +DA:81,0 +DA:83,0 +DA:87,9 +DA:89,27 +DA:90,9 +DA:91,18 +DA:94,9 +DA:96,18 +DA:99,0 +DA:101,0 +DA:102,0 +DA:105,9 +DA:108,18 +DA:109,9 +DA:110,9 +DA:111,9 +DA:112,9 +DA:113,9 +DA:114,18 +DA:115,9 +DA:120,9 +DA:121,9 +DA:122,9 +DA:123,9 +DA:124,9 +DA:125,18 +DA:126,9 +DA:131,9 +DA:132,18 +DA:137,9 +DA:138,9 +DA:139,18 +DA:140,0 +DA:142,18 +DA:143,0 +LF:52 +LH:39 +end_of_record +SF:lib/src/service/service.dart +DA:8,9 +DA:9,18 +DA:10,27 +DA:11,18 +DA:16,8 +DA:17,16 +DA:18,24 +DA:19,16 +DA:26,1 +DA:27,2 +DA:28,3 +DA:29,2 +DA:39,9 +DA:40,18 +DA:41,27 +DA:42,18 +DA:49,9 +DA:50,18 +DA:51,27 +DA:52,18 +LF:20 +LH:20 +end_of_record +SF:lib/src/service/selection_service.dart +DA:80,9 +DA:86,9 +DA:93,9 +DA:94,9 +DA:113,27 +DA:115,9 +DA:117,9 +DA:119,18 +DA:120,27 +DA:123,0 +DA:125,0 +DA:128,0 +DA:129,0 +DA:133,9 +DA:135,9 +DA:136,18 +DA:137,27 +DA:139,9 +DA:142,9 +DA:144,9 +DA:145,9 +DA:146,9 +DA:147,9 +DA:148,9 +DA:149,9 +DA:150,9 +DA:151,18 +DA:161,5 +DA:164,21 +DA:166,21 +DA:167,10 +DA:168,15 +DA:169,15 +DA:172,20 +DA:173,5 +DA:176,6 +DA:179,0 +DA:182,9 +DA:184,18 +DA:185,9 +DA:188,9 +DA:190,21 +DA:191,14 +DA:194,15 +DA:195,5 +DA:199,18 +DA:200,18 +DA:203,9 +DA:205,18 +DA:206,18 +DA:209,9 +DA:210,19 +DA:211,9 +DA:213,9 +DA:214,23 +DA:215,9 +DA:217,36 +DA:220,0 +DA:223,0 +DA:224,0 +DA:228,0 +DA:232,0 +DA:234,0 +DA:235,0 +DA:237,0 +DA:240,0 +DA:243,0 +DA:245,0 +DA:247,0 +DA:251,0 +DA:252,0 +DA:254,0 +DA:256,0 +DA:259,0 +DA:260,0 +DA:261,0 +DA:262,0 +DA:264,0 +DA:267,0 +DA:269,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:277,0 +DA:280,0 +DA:281,0 +DA:282,0 +DA:284,0 +DA:286,0 +DA:289,0 +DA:290,0 +DA:292,0 +DA:293,0 +DA:295,0 +DA:298,0 +DA:299,0 +DA:303,0 +DA:305,0 +DA:306,0 +DA:308,0 +DA:309,0 +DA:311,0 +DA:312,0 +DA:317,0 +DA:318,0 +DA:320,0 +DA:321,0 +DA:322,0 +DA:323,0 +DA:324,0 +DA:327,0 +DA:330,0 +DA:334,5 +DA:335,5 +DA:337,5 +DA:344,11 +DA:345,5 +DA:347,9 +DA:348,10 +DA:350,15 +DA:351,5 +DA:352,5 +DA:357,5 +DA:367,5 +DA:368,5 +DA:369,10 +DA:370,15 +DA:371,10 +DA:373,4 +DA:374,4 +DA:375,4 +DA:380,5 +DA:381,10 +DA:384,5 +DA:386,15 +DA:388,5 +DA:389,10 +DA:390,10 +DA:391,5 +DA:395,10 +DA:399,20 +DA:402,15 +DA:403,10 +DA:407,7 +DA:408,35 +DA:411,0 +DA:415,14 +DA:417,7 +DA:420,7 +DA:421,7 +DA:422,7 +DA:424,7 +DA:425,14 +DA:426,7 +DA:428,14 +DA:429,7 +DA:433,14 +DA:434,21 +DA:435,28 +DA:437,7 +DA:441,7 +DA:442,22 +DA:445,9 +DA:446,36 +DA:447,9 +DA:448,18 +DA:449,9 +DA:453,18 +DA:455,36 +DA:456,9 +DA:457,9 +DA:461,18 +DA:462,1 +DA:463,0 +DA:464,1 +DA:465,5 +DA:467,18 +DA:468,8 +DA:469,15 +DA:474,0 +DA:476,0 +DA:481,0 +DA:482,0 +DA:483,0 +DA:484,0 +DA:485,0 +DA:487,0 +DA:490,0 +DA:491,0 +DA:492,0 +DA:493,0 +DA:494,0 +DA:498,0 +DA:504,0 +DA:505,0 +DA:506,0 +DA:509,9 +DA:510,36 +DA:511,45 +DA:514,9 +DA:515,9 +DA:518,0 +DA:521,0 +DA:523,0 +DA:524,0 +DA:525,0 +DA:526,0 +DA:527,0 +DA:531,0 +DA:532,0 +DA:533,0 +DA:534,0 +DA:535,0 +DA:536,0 +DA:538,0 +DA:539,0 +DA:543,0 +DA:544,0 +DA:548,0 +DA:550,0 +LF:221 +LH:122 +end_of_record +SF:lib/src/service/scroll_service.dart +DA:21,9 +DA:24,9 +DA:28,9 +DA:29,9 +DA:39,9 +DA:40,27 +DA:42,1 +DA:44,3 +DA:45,2 +DA:48,1 +DA:49,3 +DA:51,1 +DA:52,3 +DA:54,1 +DA:56,1 +DA:57,3 +DA:58,3 +DA:63,9 +DA:65,9 +DA:66,9 +DA:67,9 +DA:68,9 +DA:70,9 +DA:71,18 +DA:76,5 +DA:78,15 +DA:79,5 +DA:80,15 +DA:81,15 +DA:86,1 +DA:88,1 +DA:89,4 +DA:92,1 +DA:94,1 +DA:95,4 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +LF:39 +LH:35 +end_of_record +SF:lib/src/service/keyboard_service.dart +DA:19,9 +DA:24,9 +DA:30,9 +DA:31,9 +DA:40,9 +DA:42,9 +DA:43,9 +DA:44,9 +DA:45,9 +DA:46,18 +DA:50,9 +DA:52,18 +DA:54,9 +DA:57,1 +DA:59,1 +DA:60,2 +DA:63,0 +DA:65,0 +DA:66,0 +DA:69,8 +DA:71,24 +DA:73,8 +DA:77,24 +DA:80,24 +DA:83,8 +DA:85,7 +DA:87,7 +DA:95,1 +DA:96,3 +DA:99,0 +DA:100,0 +DA:104,0 +LF:32 +LH:26 +end_of_record +SF:lib/src/service/input_service.dart +DA:18,9 +DA:22,9 +DA:27,9 +DA:28,9 +DA:36,27 +DA:38,9 +DA:40,9 +DA:42,36 +DA:43,18 +DA:46,9 +DA:48,9 +DA:49,36 +DA:50,18 +DA:52,9 +DA:55,9 +DA:57,9 +DA:58,18 +DA:62,9 +DA:64,18 +DA:74,9 +DA:75,9 +DA:76,9 +DA:79,1 +DA:82,2 +DA:83,1 +DA:85,1 +DA:86,1 +DA:87,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:91,0 +DA:95,1 +DA:96,1 +DA:97,1 +DA:98,4 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:105,2 +DA:110,1 +DA:111,3 +DA:112,2 +DA:116,1 +DA:117,2 +DA:118,2 +DA:119,1 +DA:121,1 +DA:122,1 +DA:124,1 +DA:130,0 +DA:131,0 +DA:132,0 +DA:136,0 +DA:137,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:165,9 +DA:167,18 +DA:168,9 +DA:171,0 +DA:176,0 +DA:178,0 +DA:180,0 +DA:182,0 +DA:184,0 +DA:189,0 +DA:194,0 +DA:199,0 +DA:204,0 +DA:209,0 +DA:214,0 +DA:219,0 +DA:221,0 +DA:223,0 +DA:226,0 +DA:231,9 +DA:232,36 +DA:233,9 +DA:235,45 +DA:237,9 +DA:238,9 +DA:239,27 +DA:240,9 +DA:241,9 +DA:243,9 +DA:244,18 +DA:245,18 +DA:247,9 +DA:250,18 +DA:251,16 +DA:260,8 +DA:261,8 +DA:264,7 +DA:265,7 +DA:267,7 +DA:268,7 +DA:269,14 +DA:271,7 +DA:272,7 +DA:273,7 +LF:114 +LH:70 +end_of_record +SF:lib/src/document/node_iterator.dart +DA:14,5 +DA:18,5 +DA:20,5 +DA:21,10 +DA:22,5 +DA:26,5 +DA:31,15 +DA:32,5 +DA:36,10 +DA:37,0 +DA:38,5 +DA:39,10 +DA:41,0 +DA:42,0 +DA:44,0 +DA:46,0 +DA:50,5 +DA:53,0 +DA:54,0 +DA:55,0 +DA:60,5 +DA:62,5 +DA:65,5 +DA:66,5 +DA:68,5 +DA:69,10 +LF:26 +LH:18 +end_of_record +SF:lib/src/extensions/path_extensions.dart +DA:6,8 +DA:7,22 +DA:8,16 +DA:9,24 +DA:16,6 +DA:17,18 +DA:18,12 +DA:19,18 +DA:26,1 +DA:27,1 +DA:28,1 +DA:31,1 +DA:33,1 +DA:34,2 +LF:14 +LH:14 +end_of_record +SF:lib/src/undo_manager.dart +DA:19,7 +DA:26,0 +DA:27,0 +DA:30,10 +DA:32,0 +DA:33,0 +DA:36,7 +DA:37,14 +DA:41,1 +DA:42,1 +DA:43,5 +DA:44,2 +DA:45,1 +DA:46,1 +DA:48,2 +DA:49,2 +DA:50,1 +DA:58,10 +DA:60,7 +DA:61,28 +DA:62,0 +DA:64,14 +DA:67,1 +DA:68,2 +DA:71,2 +DA:73,2 +DA:78,0 +DA:79,0 +DA:82,15 +DA:84,21 +DA:86,0 +DA:94,10 +DA:95,10 +DA:96,10 +DA:98,7 +DA:99,14 +DA:100,7 +DA:101,14 +DA:104,10 +DA:105,5 +DA:106,0 +DA:107,0 +DA:108,0 +DA:114,1 +DA:115,2 +DA:116,1 +DA:120,2 +DA:124,1 +DA:125,1 +DA:133,1 +DA:134,2 +DA:135,1 +DA:139,2 +DA:143,1 +DA:144,1 +LF:55 +LH:44 +end_of_record +SF:lib/src/extensions/color_extension.dart +DA:6,0 +DA:7,0 +DA:8,0 +DA:13,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:30,0 +DA:33,0 +DA:34,0 +LF:15 +LH:0 +end_of_record +SF:lib/src/extensions/node_extensions.dart +DA:9,9 +DA:10,36 +DA:12,3 +DA:13,36 +DA:15,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:23,9 +DA:24,9 +DA:25,18 +DA:26,27 +LF:12 +LH:8 +end_of_record +SF:lib/src/extensions/object_extensions.dart +DA:2,9 +DA:3,9 +LF:2 +LH:2 +end_of_record +SF:lib/src/extensions/text_node_extensions.dart +DA:9,1 +DA:10,1 +DA:12,1 +DA:13,1 +DA:15,1 +DA:16,1 +DA:18,1 +DA:19,1 +DA:21,2 +DA:22,4 +DA:24,6 +DA:26,6 +DA:28,4 +DA:29,2 +DA:32,2 +DA:33,6 +DA:34,2 +DA:35,4 +DA:36,6 +DA:40,2 +DA:45,1 +DA:46,2 +DA:48,3 +DA:50,3 +DA:52,2 +DA:53,1 +DA:56,1 +DA:57,3 +DA:58,1 +DA:59,2 +DA:60,3 +DA:64,1 +DA:71,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:81,0 +DA:83,1 +DA:84,1 +DA:87,2 +DA:88,2 +DA:90,3 +DA:91,1 +DA:93,5 +DA:94,1 +DA:95,4 +DA:97,3 +DA:98,4 +DA:99,1 +DA:100,2 +DA:103,1 +DA:104,2 +DA:105,4 +DA:108,1 +LF:57 +LH:49 +end_of_record +SF:lib/src/render/rich_text/rich_text_style.dart +DA:46,0 +DA:53,3 +DA:69,6 +DA:70,2 +DA:71,2 +DA:72,2 +DA:73,2 +DA:74,2 +DA:79,2 +DA:80,6 +DA:81,2 +DA:86,2 +DA:87,2 +DA:88,6 +DA:93,0 +DA:94,0 +DA:97,0 +DA:98,0 +DA:104,1 +DA:105,3 +DA:106,1 +DA:111,9 +DA:112,9 +DA:113,0 +DA:118,4 +DA:119,12 +DA:120,4 +DA:127,9 +DA:128,13 +DA:131,9 +DA:132,13 +DA:135,9 +DA:136,9 +DA:137,4 +DA:140,9 +DA:141,9 +DA:142,4 +DA:145,9 +DA:146,13 +DA:147,2 +DA:148,4 +DA:154,9 +DA:155,9 +DA:156,0 +DA:157,0 +DA:158,0 +DA:164,0 +DA:169,9 +DA:170,9 +DA:171,0 +DA:179,9 +DA:185,4 +DA:186,12 +DA:192,27 +DA:194,4 +DA:198,9 +DA:199,9 +DA:200,9 +DA:201,9 +DA:202,9 +DA:203,9 +DA:204,9 +DA:205,9 +DA:206,9 +DA:207,9 +DA:210,9 +DA:214,9 +DA:215,9 +DA:216,0 +DA:217,0 +DA:218,0 +DA:219,0 +DA:220,0 +DA:226,9 +DA:227,18 +DA:234,9 +DA:235,9 +DA:236,34 +DA:237,2 +DA:240,18 +DA:241,2 +DA:243,9 +DA:247,9 +DA:248,18 +DA:251,9 +DA:252,18 +DA:255,18 +DA:258,9 +DA:259,18 +DA:260,0 +DA:261,18 +DA:262,0 +DA:268,9 +DA:273,9 +DA:274,18 +DA:276,0 +DA:277,0 +LF:97 +LH:77 +end_of_record +SF:lib/src/infra/flowy_svg.dart +DA:5,8 +DA:12,8 +DA:20,8 +DA:22,8 +DA:23,8 +DA:24,8 +DA:28,8 +DA:29,8 +DA:30,8 +DA:31,8 +DA:32,8 +DA:33,16 +DA:34,8 +DA:39,1 +DA:41,2 +DA:42,1 +DA:44,2 +DA:45,2 +DA:48,0 +LF:19 +LH:18 +end_of_record +SF:lib/src/infra/html_converter.dart +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:85,0 +DA:88,0 +DA:91,0 +DA:92,0 +DA:97,0 +DA:98,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:114,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:133,0 +DA:134,0 +DA:135,0 +DA:136,0 +DA:139,0 +DA:142,0 +DA:144,0 +DA:145,0 +DA:146,0 +DA:150,0 +DA:151,0 +DA:156,0 +DA:157,0 +DA:158,0 +DA:159,0 +DA:162,0 +DA:168,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:174,0 +DA:176,0 +DA:177,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:186,0 +DA:188,0 +DA:191,0 +DA:194,0 +DA:196,0 +DA:197,0 +DA:200,0 +DA:201,0 +DA:204,0 +DA:207,0 +DA:208,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:212,0 +DA:213,0 +DA:218,0 +DA:219,0 +DA:220,0 +DA:221,0 +DA:222,0 +DA:223,0 +DA:226,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:232,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:239,0 +DA:247,0 +DA:249,0 +DA:251,0 +DA:254,0 +DA:257,0 +DA:259,0 +DA:260,0 +DA:263,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:269,0 +DA:274,0 +DA:276,0 +DA:277,0 +DA:282,0 +DA:283,0 +DA:284,0 +DA:286,0 +DA:288,0 +DA:291,0 +DA:292,0 +DA:293,0 +DA:294,0 +DA:295,0 +DA:300,0 +DA:301,0 +DA:302,0 +DA:303,0 +DA:304,0 +DA:305,0 +DA:310,0 +DA:314,0 +DA:315,0 +DA:316,0 +DA:318,0 +DA:322,0 +DA:324,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:328,0 +DA:354,0 +DA:356,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:361,0 +DA:362,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:370,0 +DA:371,0 +DA:372,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:381,0 +DA:382,0 +DA:383,0 +DA:384,0 +DA:386,0 +DA:391,0 +DA:392,0 +DA:393,0 +DA:395,0 +DA:398,0 +DA:399,0 +DA:402,0 +DA:403,0 +DA:404,0 +DA:405,0 +DA:407,0 +DA:409,0 +DA:410,0 +DA:411,0 +DA:413,0 +DA:417,0 +DA:418,0 +DA:419,0 +DA:420,0 +DA:424,0 +DA:425,0 +DA:426,0 +DA:427,0 +DA:431,0 +DA:434,0 +DA:435,0 +DA:436,0 +DA:437,0 +DA:439,0 +DA:440,0 +DA:443,0 +DA:446,0 +DA:447,0 +DA:448,0 +DA:449,0 +DA:450,0 +DA:452,0 +DA:454,0 +DA:455,0 +DA:456,0 +DA:458,0 +DA:460,0 +DA:461,0 +DA:464,0 +DA:465,0 +DA:466,0 +DA:469,0 +DA:470,0 +DA:472,0 +DA:475,0 +DA:476,0 +DA:477,0 +DA:478,0 +DA:481,0 +DA:501,0 +DA:504,0 +DA:507,0 +DA:510,0 +DA:512,0 +DA:513,0 +DA:515,0 +DA:517,0 +DA:518,0 +DA:519,0 +DA:521,0 +DA:523,0 +DA:526,0 +DA:530,0 +DA:531,0 +DA:532,0 +DA:534,0 +DA:535,0 +DA:536,0 +DA:537,0 +DA:538,0 +DA:539,0 +DA:540,0 +DA:541,0 +DA:542,0 +DA:543,0 +DA:544,0 +DA:545,0 +DA:546,0 +DA:547,0 +DA:548,0 +DA:549,0 +DA:550,0 +DA:551,0 +DA:552,0 +DA:554,0 +DA:555,0 +DA:556,0 +DA:557,0 +DA:559,0 +DA:560,0 +DA:563,0 +DA:568,0 +DA:569,0 +DA:570,0 +DA:571,0 +DA:573,0 +DA:574,0 +DA:576,0 +DA:577,0 +DA:578,0 +DA:579,0 +DA:581,0 +DA:582,0 +DA:585,0 +DA:586,0 +DA:587,0 +DA:594,0 +DA:595,0 +DA:596,0 +DA:599,0 +DA:600,0 +LF:299 +LH:0 +end_of_record +SF:lib/src/render/editor/editor_entry.dart +DA:8,9 +DA:10,9 +DA:11,18 +DA:12,9 +DA:13,9 +DA:17,9 +DA:18,9 +DA:19,18 +DA:24,9 +DA:28,9 +DA:33,9 +DA:35,9 +DA:37,18 +DA:38,9 +DA:39,9 +DA:40,36 +DA:41,9 +DA:42,9 +DA:45,9 +DA:47,0 +DA:50,0 +DA:54,9 +LF:22 +LH:20 +end_of_record +SF:lib/src/render/rich_text/bulleted_list_text.dart +DA:12,3 +DA:14,3 +DA:15,6 +DA:16,3 +DA:17,3 +DA:21,3 +DA:22,3 +DA:28,3 +DA:32,3 +DA:37,3 +DA:39,3 +DA:53,3 +DA:55,6 +DA:57,3 +DA:59,12 +DA:61,3 +DA:63,3 +DA:64,3 +DA:65,3 +DA:67,3 +DA:68,3 +DA:69,3 +DA:70,6 +DA:72,6 +DA:75,3 +DA:76,3 +DA:77,3 +DA:79,6 +DA:80,6 +LF:29 +LH:29 +end_of_record +SF:lib/src/render/rich_text/default_selectable.dart +DA:11,18 +DA:12,18 +DA:13,30 +DA:14,10 +DA:15,30 +DA:21,0 +DA:22,0 +DA:24,18 +DA:25,72 +DA:27,10 +DA:28,5 +DA:29,20 +DA:30,5 +DA:32,0 +DA:33,0 +DA:35,60 +DA:37,0 +DA:38,0 +DA:40,15 +DA:42,15 +LF:20 +LH:14 +end_of_record +SF:lib/src/render/rich_text/flowy_rich_text.dart +DA:19,9 +DA:28,9 +DA:38,9 +DA:39,9 +DA:48,9 +DA:49,27 +DA:51,2 +DA:52,6 +DA:54,9 +DA:56,9 +DA:59,5 +DA:60,20 +DA:62,5 +DA:63,5 +DA:64,35 +DA:66,7 +DA:68,14 +DA:70,14 +DA:71,14 +DA:72,14 +DA:73,4 +DA:76,7 +DA:77,35 +DA:78,7 +DA:79,14 +DA:85,0 +DA:87,0 +DA:88,0 +DA:89,0 +DA:92,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:102,5 +DA:104,25 +DA:105,30 +DA:107,5 +DA:108,10 +DA:109,10 +DA:111,5 +DA:112,5 +DA:113,15 +DA:114,5 +DA:117,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:130,9 +DA:131,9 +DA:133,36 +DA:134,2 +DA:135,2 +DA:136,2 +DA:137,2 +DA:140,9 +DA:144,2 +DA:145,2 +DA:146,2 +DA:147,2 +DA:150,4 +DA:151,6 +DA:156,9 +DA:157,9 +DA:158,9 +DA:159,9 +DA:162,18 +DA:163,12 +DA:169,0 +DA:170,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:175,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:181,0 +DA:185,0 +DA:190,9 +DA:192,18 +DA:195,18 +DA:196,27 +DA:197,9 +DA:198,27 +DA:199,17 +DA:200,9 +DA:201,9 +DA:202,9 +DA:203,9 +DA:206,6 +DA:207,2 +DA:208,4 +DA:209,2 +DA:212,2 +DA:213,2 +LF:101 +LH:72 +end_of_record +SF:lib/src/render/rich_text/checkbox_text.dart +DA:13,4 +DA:15,4 +DA:16,8 +DA:17,4 +DA:18,4 +DA:22,4 +DA:23,4 +DA:24,8 +DA:29,4 +DA:33,4 +DA:38,4 +DA:39,4 +DA:51,4 +DA:53,8 +DA:55,4 +DA:57,16 +DA:58,4 +DA:60,0 +DA:64,4 +DA:65,16 +DA:66,16 +DA:67,4 +DA:69,4 +DA:70,4 +DA:71,4 +DA:73,4 +DA:74,4 +DA:75,4 +DA:76,4 +DA:77,8 +DA:78,4 +DA:79,4 +DA:82,1 +DA:83,2 +DA:84,3 +DA:85,4 +DA:88,1 +DA:91,4 +DA:92,4 +DA:93,4 +DA:95,8 +DA:96,4 +DA:97,4 +DA:98,8 +DA:106,0 +DA:107,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:125,0 +DA:127,0 +DA:130,0 +DA:134,0 +DA:142,4 +DA:143,4 +DA:144,4 +DA:145,4 +DA:146,4 +DA:147,8 +DA:148,4 +DA:149,16 +DA:150,8 +DA:152,8 +DA:154,8 +DA:155,12 +DA:158,3 +DA:159,4 +DA:162,4 +LF:76 +LH:58 +end_of_record +SF:lib/src/render/rich_text/heading_text.dart +DA:11,2 +DA:13,2 +DA:14,4 +DA:15,2 +DA:16,2 +DA:20,2 +DA:21,2 +DA:22,4 +DA:27,2 +DA:31,2 +DA:36,2 +DA:37,2 +DA:44,0 +DA:50,2 +DA:52,4 +DA:54,2 +DA:56,4 +DA:59,2 +DA:61,2 +DA:62,2 +DA:63,2 +DA:66,2 +DA:68,2 +DA:69,2 +DA:71,2 +DA:72,2 +DA:73,4 +DA:74,4 +DA:80,2 +DA:81,2 +DA:82,2 +DA:83,2 +DA:84,2 +DA:85,4 +DA:86,2 +DA:87,4 +DA:88,8 +DA:90,2 +DA:93,2 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:107,0 +DA:110,0 +LF:50 +LH:38 +end_of_record +SF:lib/src/render/rich_text/number_list_text.dart +DA:12,1 +DA:14,1 +DA:15,2 +DA:16,1 +DA:17,1 +DA:21,1 +DA:22,1 +DA:23,2 +DA:28,1 +DA:32,1 +DA:37,1 +DA:39,1 +DA:53,1 +DA:55,2 +DA:57,1 +DA:59,4 +DA:60,1 +DA:61,1 +DA:62,1 +DA:64,1 +DA:66,1 +DA:67,1 +DA:68,1 +DA:69,2 +DA:71,2 +DA:72,4 +DA:74,1 +DA:75,1 +DA:76,1 +DA:78,2 +DA:79,2 +LF:31 +LH:31 +end_of_record +SF:lib/src/render/rich_text/quoted_text.dart +DA:12,2 +DA:14,2 +DA:15,4 +DA:16,2 +DA:17,2 +DA:21,2 +DA:22,2 +DA:28,2 +DA:32,2 +DA:37,2 +DA:38,2 +DA:52,2 +DA:54,4 +DA:56,2 +DA:58,8 +DA:59,2 +DA:61,2 +DA:62,2 +DA:63,2 +DA:65,2 +DA:66,2 +DA:67,2 +DA:68,6 +DA:70,4 +DA:73,2 +DA:74,2 +DA:75,2 +DA:77,4 +DA:78,4 +DA:86,2 +DA:88,16 +DA:89,6 +LF:32 +LH:32 +end_of_record +SF:lib/src/render/rich_text/rich_text.dart +DA:12,8 +DA:14,8 +DA:15,16 +DA:16,8 +DA:17,8 +DA:21,8 +DA:22,8 +DA:28,8 +DA:32,8 +DA:37,8 +DA:38,8 +DA:45,8 +DA:50,8 +DA:52,16 +DA:54,8 +DA:56,8 +DA:58,8 +DA:59,8 +DA:60,8 +DA:61,8 +DA:62,16 +DA:63,16 +LF:22 +LH:22 +end_of_record +SF:lib/src/render/selection/cursor_widget.dart +DA:6,7 +DA:12,7 +DA:19,7 +DA:20,7 +DA:27,7 +DA:29,7 +DA:31,14 +DA:34,7 +DA:36,14 +DA:37,7 +DA:40,7 +DA:41,7 +DA:42,35 +DA:43,2 +DA:44,4 +DA:45,4 +DA:51,4 +DA:52,8 +DA:53,4 +DA:55,8 +DA:56,8 +DA:59,7 +DA:61,7 +DA:62,14 +DA:63,7 +DA:64,14 +DA:65,21 +DA:69,7 +DA:70,7 +DA:71,21 +LF:30 +LH:30 +end_of_record +SF:lib/src/render/selection/selection_widget.dart +DA:4,5 +DA:9,5 +DA:15,5 +DA:16,5 +DA:20,5 +DA:22,5 +DA:23,10 +DA:24,5 +DA:25,10 +DA:26,15 +DA:30,5 +DA:31,5 +DA:32,10 +LF:13 +LH:13 +end_of_record +SF:lib/src/render/selection/toolbar_widget.dart +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:38,5 +DA:44,5 +DA:51,5 +DA:52,5 +DA:68,5 +DA:70,5 +DA:71,15 +DA:72,15 +DA:73,5 +DA:74,10 +DA:76,10 +DA:77,5 +DA:82,5 +DA:84,5 +DA:85,5 +DA:88,5 +DA:89,5 +DA:90,10 +DA:92,5 +DA:93,5 +DA:94,5 +DA:96,5 +DA:97,5 +DA:98,5 +DA:99,5 +DA:100,5 +DA:101,5 +DA:102,5 +DA:103,5 +DA:104,5 +DA:105,5 +DA:106,5 +DA:113,5 +DA:114,5 +DA:116,5 +DA:117,5 +DA:118,0 +DA:122,5 +DA:124,5 +DA:128,5 +DA:129,0 +DA:130,5 +DA:132,10 +DA:133,10 +DA:134,5 +DA:135,5 +DA:136,5 +DA:144,0 +DA:146,0 +DA:148,0 +DA:150,0 +DA:151,0 +DA:152,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:164,0 +DA:165,0 +DA:166,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:177,0 +DA:179,0 +DA:180,0 +DA:187,0 +DA:188,0 +DA:198,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:206,0 +LF:89 +LH:45 +end_of_record +SF:lib/src/service/default_text_operations/format_rich_text_style.dart +DA:11,0 +DA:12,0 +DA:18,0 +DA:19,0 +DA:24,0 +DA:25,0 +DA:31,0 +DA:32,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:54,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:65,0 +DA:66,0 +DA:69,0 +DA:70,0 +DA:76,0 +DA:77,0 +DA:82,0 +DA:83,0 +DA:89,0 +DA:90,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:99,0 +DA:103,0 +DA:105,0 +DA:107,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:122,0 +DA:126,1 +DA:127,1 +DA:130,1 +DA:131,1 +DA:134,1 +DA:135,1 +DA:138,1 +DA:139,1 +DA:142,1 +DA:143,4 +DA:144,3 +DA:145,2 +DA:147,1 +DA:151,1 +DA:152,1 +DA:156,1 +DA:159,1 +DA:160,4 +DA:161,3 +DA:167,1 +DA:168,1 +DA:170,2 +DA:171,1 +DA:175,1 +DA:180,5 +DA:181,1 +DA:182,1 +DA:183,2 +DA:184,5 +DA:188,3 +DA:189,1 +DA:191,2 +DA:192,3 +DA:193,2 +DA:194,5 +DA:195,5 +DA:196,2 +DA:198,1 +DA:207,1 +LF:87 +LH:39 +end_of_record +SF:lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +DA:13,27 +DA:14,9 +DA:15,9 +DA:16,9 +DA:17,9 +DA:18,9 +DA:19,9 +DA:20,9 +DA:21,9 +DA:22,9 +DA:23,9 +LF:11 +LH:11 +end_of_record +SF:lib/src/service/toolbar_service.dart +DA:16,9 +DA:20,9 +DA:25,9 +DA:26,9 +DA:34,5 +DA:36,5 +DA:38,10 +DA:39,10 +DA:40,5 +DA:41,10 +DA:43,10 +DA:47,20 +DA:50,9 +DA:52,28 +DA:53,14 +DA:54,9 +DA:57,9 +DA:59,9 +DA:60,18 +DA:64,9 +DA:66,9 +DA:68,9 +LF:22 +LH:22 +end_of_record +SF:lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +DA:5,0 +DA:6,0 +DA:7,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:37,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:69,0 +DA:72,2 +DA:73,2 +DA:78,4 +DA:79,0 +DA:80,0 +DA:82,0 +DA:84,4 +DA:85,0 +DA:86,0 +DA:88,0 +DA:90,4 +DA:91,0 +DA:92,0 +DA:94,0 +DA:96,4 +DA:97,0 +DA:98,0 +DA:100,0 +DA:106,24 +DA:107,6 +DA:108,2 +DA:111,6 +DA:116,10 +DA:117,0 +DA:118,0 +DA:120,0 +DA:124,0 +DA:127,10 +DA:128,0 +DA:129,0 +DA:131,0 +DA:134,0 +DA:137,10 +DA:138,0 +DA:139,0 +DA:140,0 +DA:142,10 +DA:143,0 +DA:144,0 +DA:145,0 +LF:75 +LH:14 +end_of_record +SF:lib/src/service/internal_key_event_handlers/copy_paste_handler.dart +DA:8,0 +DA:9,0 +DA:10,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:44,0 +DA:45,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:57,0 +DA:58,0 +DA:60,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:79,0 +DA:82,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:89,0 +DA:93,0 +DA:95,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:113,0 +DA:116,0 +DA:117,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:126,0 +DA:127,0 +DA:129,0 +DA:130,0 +DA:134,0 +DA:136,0 +DA:137,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:146,0 +DA:147,0 +DA:152,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:156,0 +DA:158,0 +DA:159,0 +DA:160,0 +DA:161,0 +DA:162,0 +DA:163,0 +DA:168,0 +DA:169,0 +DA:170,0 +DA:172,0 +DA:176,0 +DA:177,0 +DA:178,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:185,0 +DA:186,0 +DA:192,0 +DA:193,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:203,0 +DA:205,0 +DA:207,0 +DA:209,0 +DA:210,0 +DA:211,0 +DA:213,0 +DA:214,0 +DA:219,0 +DA:220,0 +DA:222,0 +DA:224,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:229,0 +DA:231,0 +DA:232,0 +DA:234,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:241,0 +DA:242,0 +DA:245,0 +DA:246,0 +DA:252,0 +DA:253,0 +DA:254,0 +DA:255,0 +DA:258,0 +DA:259,0 +DA:260,0 +DA:263,0 +DA:264,0 +DA:265,0 +DA:266,0 +DA:268,0 +DA:269,0 +DA:270,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:279,0 +DA:281,0 +DA:282,0 +DA:283,0 +DA:284,0 +DA:286,0 +DA:287,0 +DA:288,0 +DA:289,0 +DA:290,0 +DA:292,0 +DA:293,0 +DA:294,0 +DA:300,0 +DA:303,0 +DA:304,0 +DA:307,24 +DA:308,12 +DA:309,0 +DA:312,12 +DA:313,0 +DA:316,12 +DA:317,0 +LF:178 +LH:4 +end_of_record +SF:lib/src/service/internal_key_event_handlers/delete_text_handler.dart +DA:6,2 +DA:7,8 +DA:11,6 +DA:12,6 +DA:13,4 +DA:15,4 +DA:16,6 +DA:20,2 +DA:21,4 +DA:22,1 +DA:23,4 +DA:24,2 +DA:26,1 +DA:28,2 +DA:31,2 +DA:32,1 +DA:33,1 +DA:40,1 +DA:41,2 +DA:42,1 +DA:44,1 +DA:45,1 +DA:46,2 +DA:47,1 +DA:48,1 +DA:49,2 +DA:57,1 +DA:58,1 +DA:61,3 +DA:64,1 +DA:66,2 +DA:67,5 +DA:72,2 +DA:75,4 +DA:76,2 +DA:82,1 +DA:83,4 +DA:87,3 +DA:88,3 +DA:89,2 +DA:91,2 +DA:92,3 +DA:96,1 +DA:97,2 +DA:98,1 +DA:99,5 +DA:100,2 +DA:101,1 +DA:105,1 +DA:106,1 +DA:108,1 +DA:110,4 +DA:111,1 +DA:112,1 +DA:114,2 +DA:115,3 +DA:118,1 +DA:120,2 +DA:121,5 +DA:126,1 +DA:129,1 +DA:134,2 +DA:136,2 +DA:137,2 +DA:138,4 +DA:139,8 +DA:143,4 +DA:144,2 +DA:147,4 +DA:148,4 +DA:153,26 +DA:154,16 +DA:155,2 +DA:157,16 +DA:158,1 +LF:75 +LH:75 +end_of_record +SF:lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +DA:21,18 +DA:22,5 +DA:23,11 +DA:27,4 +DA:28,3 +DA:32,1 +DA:33,1 +DA:34,2 +DA:36,2 +DA:38,3 +DA:43,1 +DA:44,1 +DA:46,4 +DA:47,1 +DA:48,4 +DA:50,1 +DA:51,1 +DA:52,1 +DA:53,2 +DA:54,3 +DA:56,1 +DA:57,1 +DA:58,1 +DA:60,2 +DA:62,1 +DA:63,1 +DA:68,2 +DA:72,1 +DA:76,4 +DA:77,3 +DA:78,1 +DA:79,2 +DA:81,1 +DA:82,1 +DA:84,1 +DA:85,1 +DA:86,1 +DA:88,1 +DA:89,1 +DA:91,1 +DA:92,3 +DA:94,1 +DA:95,1 +DA:96,1 +DA:97,1 +DA:99,1 +DA:100,1 +DA:107,1 +DA:108,3 +DA:109,2 +DA:110,1 +DA:112,2 +DA:113,1 +DA:114,1 +DA:117,1 +DA:118,3 +DA:120,1 +DA:121,1 +DA:122,2 +DA:123,1 +DA:125,4 +DA:128,1 +DA:130,2 +DA:131,5 +DA:133,1 +DA:134,1 +LF:66 +LH:66 +end_of_record +SF:lib/src/service/internal_key_event_handlers/redo_undo_handler.dart +DA:5,24 +DA:6,12 +DA:7,1 +DA:8,2 +DA:10,2 +LF:5 +LH:5 +end_of_record +SF:lib/src/service/internal_key_event_handlers/slash_handler.dart +DA:14,1 +DA:15,1 +DA:17,3 +DA:18,1 +DA:20,1 +DA:21,1 +DA:22,0 +DA:23,0 +DA:26,1 +DA:28,1 +DA:29,1 +DA:30,0 +DA:31,0 +DA:33,1 +DA:35,1 +DA:36,1 +DA:37,0 +DA:38,0 +DA:40,1 +DA:42,1 +DA:43,1 +DA:44,0 +DA:45,0 +DA:47,1 +DA:49,1 +DA:50,1 +DA:51,0 +DA:59,1 +DA:61,1 +DA:62,1 +DA:63,0 +DA:70,25 +DA:71,14 +DA:75,3 +DA:76,1 +DA:77,2 +DA:81,4 +DA:82,1 +DA:83,1 +DA:84,1 +DA:88,3 +DA:89,1 +DA:92,1 +DA:93,3 +DA:94,6 +DA:95,1 +DA:98,3 +DA:101,3 +DA:102,1 +DA:103,3 +DA:104,1 +DA:106,3 +DA:108,3 +DA:114,1 +DA:116,0 +DA:117,1 +DA:118,2 +DA:119,1 +DA:120,1 +DA:121,1 +DA:123,1 +DA:128,2 +DA:131,1 +DA:136,3 +DA:139,4 +DA:148,1 +DA:151,3 +DA:152,3 +DA:157,1 +DA:162,1 +DA:168,1 +DA:169,1 +DA:180,0 +DA:181,0 +DA:182,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:187,0 +DA:188,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:195,0 +DA:198,0 +DA:199,0 +DA:201,0 +DA:202,0 +DA:203,0 +DA:208,1 +DA:210,1 +DA:212,3 +DA:214,3 +DA:215,2 +DA:219,1 +DA:221,2 +DA:223,1 +DA:226,1 +DA:228,1 +DA:229,1 +DA:230,1 +DA:231,1 +DA:232,1 +DA:234,1 +DA:235,1 +DA:238,1 +DA:241,1 +DA:243,2 +DA:244,0 +DA:245,1 +DA:247,3 +DA:253,0 +DA:268,1 +DA:269,1 +DA:270,1 +DA:271,3 +DA:272,5 +DA:273,2 +DA:277,1 +DA:279,2 +DA:280,2 +DA:281,1 +DA:282,1 +DA:285,1 +DA:286,2 +DA:290,1 +DA:295,0 +DA:296,0 +DA:297,0 +DA:301,0 +DA:308,0 +DA:309,0 +DA:310,0 +DA:311,0 +DA:314,0 +DA:315,0 +DA:317,0 +DA:318,0 +DA:319,0 +DA:321,0 +DA:323,0 +DA:325,0 +DA:326,0 +DA:327,0 +DA:328,0 +DA:332,0 +DA:333,0 +DA:334,0 +DA:335,0 +DA:336,0 +DA:337,0 +DA:338,0 +DA:339,0 +DA:340,0 +DA:342,0 +DA:343,0 +DA:344,0 +DA:351,0 +DA:353,0 +DA:355,0 +DA:356,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:361,0 +DA:364,0 +DA:368,0 +DA:370,0 +DA:372,0 +DA:373,0 +DA:375,0 +DA:376,0 +DA:377,0 +DA:378,0 +DA:381,0 +DA:387,1 +DA:392,1 +DA:398,1 +DA:400,1 +DA:402,1 +DA:404,1 +DA:405,2 +DA:406,1 +DA:408,1 +DA:411,1 +DA:412,1 +DA:413,1 +DA:415,1 +DA:416,2 +DA:423,0 +DA:424,0 +DA:433,1 +DA:448,2 +DA:449,1 +LF:194 +LH:113 +end_of_record +SF:lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +DA:8,22 +DA:9,4 +DA:13,8 +DA:14,6 +DA:15,4 +DA:17,1 +DA:21,2 +DA:22,1 +DA:24,2 +DA:25,1 +DA:27,2 +DA:28,1 +DA:30,2 +DA:31,1 +DA:32,1 +LF:15 +LH:15 +end_of_record +SF:lib/src/service/internal_key_event_handlers/whitespace_handler.dart +DA:12,1 +DA:14,1 +DA:16,1 +DA:23,21 +DA:24,6 +DA:32,4 +DA:33,1 +DA:37,3 +DA:38,1 +DA:39,2 +DA:43,1 +DA:44,1 +DA:45,3 +DA:46,1 +DA:47,2 +DA:48,1 +DA:49,2 +DA:50,1 +DA:56,1 +DA:57,2 +DA:60,1 +DA:61,1 +DA:62,2 +DA:65,2 +DA:66,1 +DA:67,1 +DA:71,1 +DA:75,1 +DA:76,2 +DA:81,1 +DA:82,3 +DA:83,1 +DA:84,1 +DA:87,1 +DA:88,3 +DA:89,1 +DA:93,1 +DA:94,2 +DA:95,2 +DA:99,2 +DA:100,1 +DA:101,1 +DA:105,1 +DA:109,1 +DA:111,1 +DA:112,1 +DA:115,1 +DA:116,3 +DA:119,1 +DA:120,1 +DA:121,2 +DA:125,2 +DA:126,1 +DA:127,1 +DA:131,1 +DA:135,1 +DA:136,2 +DA:137,5 +LF:58 +LH:58 +end_of_record +SF:lib/src/service/internal_key_event_handlers/select_all_handler.dart +DA:5,1 +DA:6,4 +DA:9,4 +DA:10,4 +DA:12,1 +DA:13,2 +DA:15,2 +DA:16,2 +DA:17,2 +DA:21,21 +DA:22,5 +DA:23,1 +LF:12 +LH:12 +end_of_record +SF:lib/src/service/internal_key_event_handlers/page_up_down_handler.dart +DA:5,20 +DA:6,4 +DA:7,3 +DA:8,2 +DA:10,3 +DA:13,4 +DA:14,3 +DA:15,2 +DA:17,3 +LF:9 +LH:9 +end_of_record +SF:lib/src/service/selection/selection_gesture.dart +DA:10,9 +DA:19,9 +DA:21,9 +DA:23,9 +DA:43,9 +DA:45,9 +DA:47,9 +DA:49,9 +DA:50,18 +DA:51,9 +DA:53,27 +DA:54,27 +DA:55,27 +DA:59,9 +DA:60,18 +DA:61,9 +DA:62,18 +DA:66,18 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:78,0 +DA:79,0 +DA:80,0 +DA:81,0 +DA:82,0 +DA:83,0 +DA:85,0 +DA:87,0 +DA:88,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:102,0 +DA:107,9 +DA:109,9 +DA:110,9 +DA:111,9 +LF:48 +LH:22 +end_of_record diff --git a/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md b/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md new file mode 100644 index 0000000000..8e264844ef --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md @@ -0,0 +1,14 @@ +# Contributing + +## Reporting Bugs +补充截图,在 Appflowy 仓库增加一个 editor bug 的截图 + +## 技术讨论与支持 +补充discord截图,editor那个群 + +## 提交PR +我们很欢迎和appreciate大家提交的PR。我们也有很多first-contributor-welcome or help-wanted 的 issue 欢迎大家一起来实现。 + +BTW: 正如ReadMe所说,我们想保证大家提交的代码不会影响到现有的代码逻辑和功能,所以每次提交PR请附上对应的test,建议在test加上对测试用例,范围的简单描述。更多细节请看test.md + +最后,重复一句,由于我们是社区驱动的开源编辑器,所以我们会认真对待每一个PR以及每一次的PR,非常感觉大家的贡献。 diff --git a/frontend/app_flowy/packages/flowy_editor/documentation/testing.md b/frontend/app_flowy/packages/flowy_editor/documentation/testing.md new file mode 100644 index 0000000000..c5253ae55e --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/documentation/testing.md @@ -0,0 +1,129 @@ +# Testing + +目前测试文件的目录结构与代码文件的目录结构是保持一致的,这样方便我们查找新增文件的测试情况,以及方便检索对应文件的测试代码路径。 + +## 提供的测试方法 + + +构造测试的文档数据 +```dart +const text = 'Welcome to Appflowy 😁'; +// 获取编辑器 +final editor = tester.editor; +// 插入空的文本节点 +editor.insertEmptyTextNode(); +// 插入带信息的文本节点 +editor.insertTextNode(text); +// 插入样式heading的文本节点 +editor.insertTextNode(text, attributes: { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: StyleKey.h1, +}); +// 插入样式bulleted list的加粗的文本节点 +editor.insertTextNode( + '', + attributes: { + StyleKey.subtype: StyleKey.bulletedList, + }, + delta: Delta([ + TextInsert(text, {StyleKey.bold: true}), + ]), +); +``` + +在测试前必须调用 +```dart +await editor.startTesting(); +``` + +获取当前渲染的节点数量 +```dart +final length = editor.documentLength; +print(length); +``` + +获取节点 +```dart +// 获取上述文档结构中的第一个文本节点 +final firstTextNode = editor.nodeAtPath([0]) as TextNode; +``` + +更新选区信息 +```dart +await editor.updateSelection( + Selection.single(path: firstTextNode.path, startOffset: 0), +); +``` + +获取选区信息 +```dart +final selection = editor.documentSelection; +print(selection); +``` + +模拟快捷键输入 +```dart +// 输入 command + A +await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); +// 输入 command + shift + S +await editor.pressLogicKey( + LogicalKeyboardKey.keyS, + isMetaPressed: true, + isShiftPressed: true, +); +``` + +模拟文字输入 +```dart +// 在第一个节点的最起始位置插入'Hello World' +editor.insertText(firstTextNode, 'Hello World', 0); +``` + +获取文本节点的信息 +```dart +// 获取纯文字 +final textAfterInserted = firstTextNode.toRawString(); +print(textAfterInserted); +// 获取文字的描述信息 +final attributes = firstTextNode.attributes; +print(attributes); +``` + +## Example +例如,目前需要测试 select_all_handler.dart 的文件 + +完整的例子 +```dart +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('select_all_handler_test.dart', () { + testWidgets('Presses Command + A in the document', (tester) async { + const lines = 100; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.pressLogicKey(LogicalKeyboardKey.keyA, isMetaPressed: true); + + expect( + editor.documentSelection, + Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [lines - 1], offset: text.length), + ), + ); + }); +} +``` + +其余关于测试的,例如模拟点击等信息请参考 [An introduction to widget testing](https://docs.flutter.dev/cookbook/testing/widget/introduction) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index d33a010b55..fdc1122e2a 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -77,8 +76,6 @@ class _MyHomePageState extends State { } else if (page == 1) { return _buildFlowyEditorWithEmptyDocument(); } else if (page == 2) { - return _buildTextField(); - } else if (page == 3) { return _buildFlowyEditorWithBigDocument(); } return Container(); @@ -115,37 +112,18 @@ class _MyHomePageState extends State { }, icon: const Icon(Icons.text_fields), ), - ActionButton( - onPressed: () { - if (page == 3) return; - setState(() { - page = 3; - }); - }, - icon: const Icon(Icons.email), - ), ], ); } Widget _buildFlowyEditorWithEmptyDocument() { - return _buildFlowyEditor( - EditorState( - document: StateTree( - root: Node( - type: 'editor', - children: LinkedList() - ..add( - TextNode.empty() - ..delta = Delta( - [TextInsert('')], - ), - ), - attributes: {}, - ), - ), - ), + final editorState = EditorState.empty(); + final editor = FlowyEditor( + editorState: editorState, + keyEventHandlers: const [], + customBuilders: const {}, ); + return editor; } Widget _buildFlowyEditorWithExample() { @@ -198,10 +176,4 @@ class _MyHomePageState extends State { ), ); } - - Widget _buildTextField() { - return const Center( - child: TextField(), - ); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart index f1437fbaef..172124bd54 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/document/state_tree.dart @@ -12,6 +12,19 @@ class StateTree { required this.root, }); + factory StateTree.empty() { + return StateTree( + root: Node.fromJson({ + 'type': 'editor', + 'children': [ + { + 'type': 'text', + } + ] + }), + ); + } + factory StateTree.fromJson(Attributes json) { assert(json['document'] is Map); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart index 4452e92ed2..50da7f4406 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/editor_state.dart @@ -75,6 +75,10 @@ class EditorState { undoManager.state = this; } + factory EditorState.empty() { + return EditorState(document: StateTree.empty()); + } + /// Apply the transaction to the state. /// /// The options can be used to determine whether the editor From 3945a648206d74e7b1b32107894f4f13fac7e31b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 15:11:19 +0800 Subject: [PATCH 158/224] chore: publish preparation --- .../packages/flowy_editor/.gitignore | 2 +- .../packages/flowy_editor/.vscode/launch.json | 45 - .../packages/flowy_editor/CHANGELOG.md | 10 +- .../app_flowy/packages/flowy_editor/README.md | 25 +- .../packages/flowy_editor/coverage/lcov.info | 3458 ----------------- .../flowy_editor/example/.vscode/launch.json | 25 - .../flowy_editor/example/pubspec.lock | 551 --- .../packages/flowy_editor/pubspec.yaml | 4 +- 8 files changed, 23 insertions(+), 4097 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/.vscode/launch.json delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json delete mode 100644 frontend/app_flowy/packages/flowy_editor/example/pubspec.lock diff --git a/frontend/app_flowy/packages/flowy_editor/.gitignore b/frontend/app_flowy/packages/flowy_editor/.gitignore index 96486fd930..d920ae6823 100644 --- a/frontend/app_flowy/packages/flowy_editor/.gitignore +++ b/frontend/app_flowy/packages/flowy_editor/.gitignore @@ -19,7 +19,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. diff --git a/frontend/app_flowy/packages/flowy_editor/.vscode/launch.json b/frontend/app_flowy/packages/flowy_editor/.vscode/launch.json deleted file mode 100644 index f27c363a13..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/.vscode/launch.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "example", - "cwd": "example", - "request": "launch", - "type": "dart" - }, - { - "name": "example (profile mode)", - "cwd": "example", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "example (release mode)", - "cwd": "example", - "request": "launch", - "type": "dart", - "flutterMode": "release" - }, - { - "name": "flowy_editor", - "request": "launch", - "type": "dart" - }, - { - "name": "flowy_editor (profile mode)", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "flowy_editor (release mode)", - "request": "launch", - "type": "dart", - "flutterMode": "release" - }, - ] -} \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md b/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md index 41cc7d8192..52359537e2 100644 --- a/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md +++ b/frontend/app_flowy/packages/flowy_editor/CHANGELOG.md @@ -1,3 +1,11 @@ ## 0.0.1 -* TODO: Describe initial release. +* 实现基础的编辑器样式 + * heading + * bold / 斜体 / underline / 删除线 + * checkbox + * bulleted list +* 实现可扩展的编辑器框架 +* IME 的支持 +* 样式的复制粘贴 +* slash 快捷输入 diff --git a/frontend/app_flowy/packages/flowy_editor/README.md b/frontend/app_flowy/packages/flowy_editor/README.md index bc41772576..84d4a2562a 100644 --- a/frontend/app_flowy/packages/flowy_editor/README.md +++ b/frontend/app_flowy/packages/flowy_editor/README.md @@ -11,13 +11,11 @@ and the Flutter guide for [developing packages and plugins](https://flutter.dev/developing-packages). --> -一个可扩展,测试覆盖的 flutter 富文本编辑组件 +一个易于扩展,测试覆盖的 flutter 富文本编辑组件 ## Features -TODO: List what your package can do. Maybe include images, gifs, or videos. - -* 可扩展的 +* 易于扩展的 * 支持扩展不同样式的视图 * 支持定制快捷键解析 * 支持扩展toolbar/popup list样式(WIP) @@ -35,14 +33,8 @@ flutter pub add flowy_editor flutter pub get ``` -TODO: List prerequisites and provide or point to information on how to -start using the package. - ## Usage -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. - Empty document ```dart final editorState = EditorState.empty(); @@ -72,18 +64,23 @@ flutter run ``` ## Examples +* 样式扩展 + * Checkbox text - 展示如何基于已有的富文本组件扩展新的样式, + * Image - 展示如何扩展新的节点,并且渲染 +* 快捷键扩展 + * BUIS - 展示如何通过快捷键对文字进行加粗/下划线/斜体/加粗 + * 粘贴HTML - 展示如何通过快捷键处理粘贴的样式 ## Documentation * 术语表 + + ## Additional information -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. 目前正在完善更多的文档信息 * Selection * -我们还有很多工作需要继续完成, +我们还有很多工作需要继续完成,链接到contributing.md Project checker link. diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info index 448f8e89a3..e69de29bb2 100644 --- a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info +++ b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info @@ -1,3458 +0,0 @@ -SF:lib/src/document/node.dart -DA:17,9 -DA:19,18 -DA:20,12 -DA:22,8 -DA:27,20 -DA:29,14 -DA:31,11 -DA:37,13 -DA:38,2 -DA:42,1 -DA:43,3 -DA:46,1 -DA:47,1 -DA:48,1 -DA:49,2 -DA:50,2 -DA:52,1 -DA:54,1 -DA:55,1 -DA:56,2 -DA:57,1 -DA:65,1 -DA:66,1 -DA:67,2 -DA:68,1 -DA:74,1 -DA:81,2 -DA:82,1 -DA:88,5 -DA:89,20 -DA:91,17 -DA:95,10 -DA:98,11 -DA:99,33 -DA:103,22 -DA:106,11 -DA:107,11 -DA:111,44 -DA:114,9 -DA:115,18 -DA:117,18 -DA:118,9 -DA:119,18 -DA:120,9 -DA:124,16 -DA:126,8 -DA:127,24 -DA:128,0 -DA:129,0 -DA:131,0 -DA:135,9 -DA:137,18 -DA:138,9 -DA:141,18 -DA:144,0 -DA:146,0 -DA:147,0 -DA:150,0 -DA:153,4 -DA:155,4 -DA:157,8 -DA:158,4 -DA:161,2 -DA:162,2 -DA:163,2 -DA:165,4 -DA:166,5 -DA:168,4 -DA:169,2 -DA:174,10 -DA:175,10 -DA:179,30 -DA:180,10 -DA:183,9 -DA:185,40 -DA:188,1 -DA:189,1 -DA:190,4 -DA:192,1 -DA:193,0 -DA:194,0 -DA:195,0 -DA:204,10 -DA:210,27 -DA:212,2 -DA:213,6 -DA:214,2 -DA:216,2 -DA:217,2 -DA:220,9 -DA:221,9 -DA:224,5 -DA:225,5 -DA:226,5 -DA:229,1 -DA:231,1 -DA:232,3 -DA:236,1 -DA:242,1 -DA:243,1 -DA:244,1 -DA:245,0 -DA:246,0 -DA:249,3 -DA:251,3 -DA:252,3 -DA:253,3 -DA:254,6 -DA:255,6 -DA:257,3 -DA:258,0 -DA:259,0 -DA:260,0 -DA:265,27 -LF:114 -LH:99 -end_of_record -SF:lib/src/document/state_tree.dart -DA:11,11 -DA:15,1 -DA:16,3 -DA:18,2 -DA:19,1 -DA:20,1 -DA:23,7 -DA:24,14 -DA:27,3 -DA:28,3 -DA:31,6 -DA:32,23 -DA:35,0 -DA:36,0 -DA:39,0 -DA:40,0 -DA:46,9 -DA:47,3 -DA:48,3 -DA:54,5 -DA:55,5 -DA:58,10 -DA:59,5 -DA:62,15 -DA:66,4 -DA:67,4 -DA:70,8 -DA:71,4 -DA:72,4 -DA:73,4 -DA:74,4 -DA:79,5 -DA:80,5 -DA:83,10 -DA:87,5 -LF:35 -LH:31 -end_of_record -SF:lib/src/document/path.dart -DA:7,10 -DA:8,10 -LF:2 -LH:2 -end_of_record -SF:lib/src/document/position.dart -DA:9,10 -DA:14,10 -DA:16,10 -DA:19,60 -DA:22,1 -DA:24,2 -DA:25,2 -DA:28,0 -DA:29,0 -DA:30,0 -DA:31,0 -DA:35,9 -DA:36,27 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -LF:17 -LH:9 -end_of_record -SF:lib/src/document/selection.dart -DA:14,6 -DA:25,7 -DA:29,7 -DA:30,7 -DA:33,6 -DA:40,40 -DA:41,54 -DA:42,8 -DA:43,80 -DA:44,48 -DA:45,6 -DA:46,60 -DA:47,20 -DA:49,0 -DA:50,0 -DA:51,0 -DA:56,12 -DA:58,1 -DA:60,3 -DA:62,3 -DA:66,5 -DA:67,5 -DA:68,5 -DA:69,5 -DA:73,20 -DA:75,0 -DA:76,0 -DA:77,0 -DA:78,0 -DA:82,9 -DA:84,9 -DA:90,18 -DA:93,0 -DA:94,0 -DA:96,9 -DA:97,27 -LF:36 -LH:27 -end_of_record -SF:lib/src/document/text_delta.dart -DA:13,21 -DA:17,4 -DA:26,10 -DA:28,9 -DA:30,18 -DA:33,10 -DA:35,10 -DA:38,5 -DA:40,5 -DA:43,15 -DA:44,15 -DA:47,0 -DA:49,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:55,1 -DA:57,1 -DA:58,1 -DA:60,1 -DA:62,3 -DA:72,7 -DA:76,7 -DA:78,14 -DA:81,7 -DA:83,7 -DA:86,0 -DA:87,0 -DA:90,6 -DA:92,6 -DA:95,1 -DA:97,1 -DA:100,6 -DA:103,0 -DA:105,0 -DA:106,0 -DA:109,1 -DA:111,1 -DA:112,1 -DA:114,1 -DA:116,3 -DA:125,6 -DA:127,6 -DA:129,6 -DA:132,1 -DA:133,1 -DA:136,1 -DA:138,1 -DA:141,3 -DA:144,0 -DA:146,0 -DA:149,1 -DA:151,1 -DA:152,1 -DA:162,7 -DA:163,7 -DA:165,6 -DA:166,12 -DA:169,6 -DA:170,24 -DA:174,18 -DA:177,6 -DA:178,24 -DA:179,18 -DA:180,18 -DA:185,6 -DA:188,24 -DA:189,4 -DA:192,18 -DA:194,6 -DA:195,6 -DA:196,12 -DA:197,6 -DA:198,12 -DA:199,6 -DA:201,10 -DA:203,6 -DA:204,4 -DA:207,6 -DA:208,5 -DA:210,5 -DA:214,6 -DA:215,6 -DA:216,18 -DA:217,6 -DA:221,0 -DA:224,5 -DA:225,5 -DA:226,5 -DA:227,4 -DA:228,3 -DA:230,2 -DA:231,2 -DA:232,2 -DA:233,6 -DA:234,2 -DA:235,2 -DA:236,4 -DA:241,1 -DA:244,2 -DA:245,1 -DA:247,2 -DA:248,2 -DA:249,1 -DA:251,2 -DA:252,2 -DA:253,2 -DA:270,2 -DA:271,2 -DA:273,3 -DA:274,1 -DA:276,1 -DA:280,2 -DA:283,19 -DA:285,2 -DA:286,4 -DA:289,7 -DA:290,7 -DA:293,6 -DA:295,12 -DA:296,10 -DA:297,7 -DA:298,3 -DA:301,15 -DA:302,10 -DA:303,12 -DA:308,5 -DA:309,4 -DA:310,4 -DA:311,4 -DA:314,6 -DA:315,0 -DA:321,12 -DA:327,7 -DA:328,7 -DA:329,14 -DA:332,13 -DA:334,6 -DA:335,10 -DA:337,11 -DA:338,6 -DA:341,12 -DA:349,3 -DA:350,6 -DA:357,7 -DA:358,14 -DA:361,18 -DA:364,5 -DA:365,10 -DA:366,15 -DA:370,6 -DA:371,12 -DA:372,12 -DA:373,6 -DA:375,6 -DA:377,6 -DA:378,5 -DA:379,5 -DA:381,20 -DA:382,8 -DA:383,4 -DA:384,4 -DA:386,15 -DA:387,12 -DA:391,6 -DA:392,12 -DA:393,10 -DA:394,3 -DA:395,3 -DA:396,10 -DA:397,1 -DA:398,1 -DA:401,15 -DA:402,5 -DA:403,5 -DA:404,5 -DA:405,15 -DA:406,15 -DA:408,5 -DA:409,1 -DA:410,5 -DA:411,10 -DA:415,5 -DA:419,5 -DA:420,10 -DA:421,15 -DA:422,10 -DA:423,10 -DA:425,8 -DA:426,2 -DA:431,6 -DA:435,5 -DA:436,10 -DA:437,10 -DA:438,6 -DA:439,6 -DA:441,5 -DA:444,7 -DA:445,14 -DA:448,6 -DA:449,12 -DA:450,16 -DA:451,6 -DA:455,1 -DA:457,1 -DA:460,3 -DA:463,0 -DA:465,0 -DA:469,7 -DA:470,7 -DA:471,20 -DA:472,6 -DA:473,6 -DA:474,11 -DA:475,10 -DA:476,10 -DA:477,7 -DA:478,5 -DA:479,10 -DA:480,10 -DA:481,5 -DA:482,4 -DA:483,4 -DA:484,4 -DA:485,6 -DA:488,5 -DA:492,7 -DA:495,2 -DA:496,8 -DA:506,2 -DA:507,2 -DA:508,2 -DA:510,2 -DA:511,6 -DA:512,6 -DA:513,6 -DA:523,2 -DA:524,2 -DA:525,6 -DA:526,1 -DA:528,6 -DA:530,10 -DA:531,6 -DA:532,4 -DA:536,2 -DA:539,10 -DA:540,10 -DA:541,60 -DA:542,10 -DA:545,10 -DA:546,20 -DA:549,2 -DA:550,4 -DA:551,4 -DA:553,2 -DA:554,6 -DA:555,8 -LF:257 -LH:241 -end_of_record -SF:lib/src/document/attributes.dart -DA:3,0 -DA:4,0 -DA:5,0 -DA:8,6 -DA:9,0 -DA:10,2 -DA:11,23 -DA:12,20 -DA:13,10 -DA:17,18 -DA:18,24 -DA:19,3 -DA:25,7 -DA:27,5 -DA:28,5 -DA:29,14 -DA:32,21 -DA:35,14 -DA:36,14 -DA:37,15 -DA:41,7 -LF:21 -LH:17 -end_of_record -SF:lib/src/editor_state.dart -DA:17,24 -DA:23,12 -DA:57,10 -DA:58,10 -DA:61,9 -DA:64,9 -DA:65,24 -DA:67,9 -DA:72,10 -DA:75,20 -DA:82,7 -DA:85,13 -DA:86,6 -DA:89,21 -DA:90,14 -DA:93,7 -DA:94,14 -DA:95,14 -DA:96,7 -DA:97,7 -DA:98,14 -DA:100,14 -DA:101,7 -DA:102,1 -DA:103,1 -DA:104,2 -DA:105,2 -DA:106,2 -DA:107,3 -DA:111,7 -DA:112,7 -DA:115,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:126,6 -DA:127,6 -DA:128,8 -DA:129,6 -DA:130,16 -DA:131,5 -DA:132,15 -DA:133,5 -DA:134,20 -LF:47 -LH:40 -end_of_record -SF:lib/src/operation/operation.dart -DA:5,0 -DA:6,0 -DA:7,0 -DA:8,0 -DA:9,0 -DA:10,0 -DA:11,0 -DA:12,0 -DA:13,0 -DA:14,0 -DA:17,0 -DA:20,8 -DA:29,0 -DA:30,0 -DA:32,0 -DA:33,0 -DA:36,6 -DA:38,1 -DA:39,2 -DA:41,1 -DA:42,1 -DA:44,1 -DA:46,1 -DA:47,1 -DA:48,1 -DA:52,1 -DA:54,1 -DA:56,2 -DA:57,4 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:73,4 -DA:77,4 -DA:79,0 -DA:81,0 -DA:82,0 -DA:84,0 -DA:85,0 -DA:87,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:92,0 -DA:96,0 -DA:98,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:110,0 -DA:111,0 -DA:113,0 -DA:114,0 -DA:117,4 -DA:120,4 -DA:122,3 -DA:123,6 -DA:125,3 -DA:126,3 -DA:128,1 -DA:130,3 -DA:133,1 -DA:135,1 -DA:137,2 -DA:138,4 -DA:147,0 -DA:148,0 -DA:149,0 -DA:150,0 -DA:151,0 -DA:154,6 -DA:158,6 -DA:160,3 -DA:161,3 -DA:162,6 -DA:164,3 -DA:165,3 -DA:167,1 -DA:169,4 -DA:172,0 -DA:174,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:183,4 -DA:184,12 -DA:187,8 -DA:191,13 -DA:192,3 -DA:196,12 -DA:197,8 -DA:198,4 -DA:199,12 -DA:200,4 -DA:201,6 -DA:203,4 -DA:205,4 -DA:209,6 -DA:210,6 -DA:211,6 -DA:212,2 -DA:213,6 -DA:214,16 -DA:215,4 -LF:106 -LH:57 -end_of_record -SF:lib/src/operation/transaction.dart -DA:21,8 -DA:27,1 -DA:28,1 -DA:29,4 -DA:31,1 -DA:32,0 -DA:34,1 -DA:35,0 -LF:8 -LH:6 -end_of_record -SF:lib/src/operation/transaction_builder.dart -DA:23,8 -DA:26,7 -DA:27,7 -DA:28,14 -DA:32,2 -DA:33,4 -DA:37,2 -DA:38,6 -DA:39,12 -DA:43,4 -DA:44,12 -DA:46,8 -DA:47,8 -DA:48,4 -DA:49,8 -DA:55,4 -DA:56,8 -DA:59,3 -DA:60,6 -DA:66,4 -DA:67,4 -DA:70,4 -DA:71,12 -DA:72,4 -DA:73,8 -DA:74,24 -DA:75,4 -DA:78,24 -DA:81,6 -DA:82,18 -DA:83,6 -DA:85,6 -DA:87,12 -DA:89,12 -DA:92,0 -DA:93,0 -DA:96,2 -DA:98,4 -DA:99,4 -DA:100,2 -DA:102,4 -DA:103,2 -DA:104,4 -DA:105,6 -DA:107,4 -DA:108,2 -DA:109,2 -DA:118,1 -DA:121,1 -DA:123,0 -DA:125,1 -DA:127,2 -DA:128,1 -DA:129,1 -DA:134,2 -DA:135,4 -DA:139,1 -DA:140,1 -DA:142,2 -DA:143,1 -DA:144,1 -DA:145,2 -DA:149,3 -DA:150,3 -DA:152,6 -DA:153,3 -DA:154,3 -DA:155,3 -DA:156,9 -DA:159,1 -DA:163,3 -DA:164,1 -DA:165,0 -DA:168,1 -DA:170,2 -DA:171,1 -DA:172,1 -DA:173,1 -DA:175,2 -DA:176,1 -DA:177,1 -DA:178,2 -DA:188,8 -DA:189,28 -DA:191,6 -DA:192,4 -DA:193,3 -DA:194,0 -DA:195,0 -DA:196,0 -DA:197,0 -DA:199,0 -DA:203,30 -DA:204,18 -DA:206,20 -DA:209,14 -DA:213,8 -DA:214,8 -DA:215,16 -DA:216,8 -DA:217,8 -LF:101 -LH:92 -end_of_record -SF:lib/src/render/selection/selectable.dart -DA:24,0 -DA:28,0 -DA:43,0 -LF:3 -LH:0 -end_of_record -SF:lib/src/service/editor_service.dart -DA:19,27 -DA:20,9 -DA:21,9 -DA:22,9 -DA:23,9 -DA:24,9 -DA:25,9 -DA:26,9 -DA:30,9 -DA:35,9 -DA:45,9 -DA:46,9 -DA:50,27 -DA:52,9 -DA:54,9 -DA:56,36 -DA:59,0 -DA:61,0 -DA:63,0 -DA:64,0 -DA:68,9 -DA:70,9 -DA:71,27 -DA:72,9 -DA:73,27 -DA:74,9 -DA:75,9 -DA:76,27 -DA:77,9 -DA:78,9 -DA:79,27 -DA:80,9 -DA:81,9 -DA:82,18 -DA:84,9 -DA:85,9 -DA:86,27 -DA:87,9 -DA:89,36 -DA:90,9 -DA:92,27 -DA:93,9 -DA:102,18 -DA:103,9 -DA:104,9 -DA:105,9 -DA:106,18 -LF:47 -LH:43 -end_of_record -SF:lib/src/service/render_plugin_service.dart -DA:39,9 -DA:45,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:53,0 -DA:59,9 -DA:63,9 -DA:69,9 -DA:71,9 -DA:73,30 -DA:74,18 -DA:75,18 -DA:76,9 -DA:77,9 -DA:78,9 -DA:80,0 -DA:81,0 -DA:83,0 -DA:87,9 -DA:89,27 -DA:90,9 -DA:91,18 -DA:94,9 -DA:96,18 -DA:99,0 -DA:101,0 -DA:102,0 -DA:105,9 -DA:108,18 -DA:109,9 -DA:110,9 -DA:111,9 -DA:112,9 -DA:113,9 -DA:114,18 -DA:115,9 -DA:120,9 -DA:121,9 -DA:122,9 -DA:123,9 -DA:124,9 -DA:125,18 -DA:126,9 -DA:131,9 -DA:132,18 -DA:137,9 -DA:138,9 -DA:139,18 -DA:140,0 -DA:142,18 -DA:143,0 -LF:52 -LH:39 -end_of_record -SF:lib/src/service/service.dart -DA:8,9 -DA:9,18 -DA:10,27 -DA:11,18 -DA:16,8 -DA:17,16 -DA:18,24 -DA:19,16 -DA:26,1 -DA:27,2 -DA:28,3 -DA:29,2 -DA:39,9 -DA:40,18 -DA:41,27 -DA:42,18 -DA:49,9 -DA:50,18 -DA:51,27 -DA:52,18 -LF:20 -LH:20 -end_of_record -SF:lib/src/service/selection_service.dart -DA:80,9 -DA:86,9 -DA:93,9 -DA:94,9 -DA:113,27 -DA:115,9 -DA:117,9 -DA:119,18 -DA:120,27 -DA:123,0 -DA:125,0 -DA:128,0 -DA:129,0 -DA:133,9 -DA:135,9 -DA:136,18 -DA:137,27 -DA:139,9 -DA:142,9 -DA:144,9 -DA:145,9 -DA:146,9 -DA:147,9 -DA:148,9 -DA:149,9 -DA:150,9 -DA:151,18 -DA:161,5 -DA:164,21 -DA:166,21 -DA:167,10 -DA:168,15 -DA:169,15 -DA:172,20 -DA:173,5 -DA:176,6 -DA:179,0 -DA:182,9 -DA:184,18 -DA:185,9 -DA:188,9 -DA:190,21 -DA:191,14 -DA:194,15 -DA:195,5 -DA:199,18 -DA:200,18 -DA:203,9 -DA:205,18 -DA:206,18 -DA:209,9 -DA:210,19 -DA:211,9 -DA:213,9 -DA:214,23 -DA:215,9 -DA:217,36 -DA:220,0 -DA:223,0 -DA:224,0 -DA:228,0 -DA:232,0 -DA:234,0 -DA:235,0 -DA:237,0 -DA:240,0 -DA:243,0 -DA:245,0 -DA:247,0 -DA:251,0 -DA:252,0 -DA:254,0 -DA:256,0 -DA:259,0 -DA:260,0 -DA:261,0 -DA:262,0 -DA:264,0 -DA:267,0 -DA:269,0 -DA:272,0 -DA:273,0 -DA:274,0 -DA:275,0 -DA:277,0 -DA:280,0 -DA:281,0 -DA:282,0 -DA:284,0 -DA:286,0 -DA:289,0 -DA:290,0 -DA:292,0 -DA:293,0 -DA:295,0 -DA:298,0 -DA:299,0 -DA:303,0 -DA:305,0 -DA:306,0 -DA:308,0 -DA:309,0 -DA:311,0 -DA:312,0 -DA:317,0 -DA:318,0 -DA:320,0 -DA:321,0 -DA:322,0 -DA:323,0 -DA:324,0 -DA:327,0 -DA:330,0 -DA:334,5 -DA:335,5 -DA:337,5 -DA:344,11 -DA:345,5 -DA:347,9 -DA:348,10 -DA:350,15 -DA:351,5 -DA:352,5 -DA:357,5 -DA:367,5 -DA:368,5 -DA:369,10 -DA:370,15 -DA:371,10 -DA:373,4 -DA:374,4 -DA:375,4 -DA:380,5 -DA:381,10 -DA:384,5 -DA:386,15 -DA:388,5 -DA:389,10 -DA:390,10 -DA:391,5 -DA:395,10 -DA:399,20 -DA:402,15 -DA:403,10 -DA:407,7 -DA:408,35 -DA:411,0 -DA:415,14 -DA:417,7 -DA:420,7 -DA:421,7 -DA:422,7 -DA:424,7 -DA:425,14 -DA:426,7 -DA:428,14 -DA:429,7 -DA:433,14 -DA:434,21 -DA:435,28 -DA:437,7 -DA:441,7 -DA:442,22 -DA:445,9 -DA:446,36 -DA:447,9 -DA:448,18 -DA:449,9 -DA:453,18 -DA:455,36 -DA:456,9 -DA:457,9 -DA:461,18 -DA:462,1 -DA:463,0 -DA:464,1 -DA:465,5 -DA:467,18 -DA:468,8 -DA:469,15 -DA:474,0 -DA:476,0 -DA:481,0 -DA:482,0 -DA:483,0 -DA:484,0 -DA:485,0 -DA:487,0 -DA:490,0 -DA:491,0 -DA:492,0 -DA:493,0 -DA:494,0 -DA:498,0 -DA:504,0 -DA:505,0 -DA:506,0 -DA:509,9 -DA:510,36 -DA:511,45 -DA:514,9 -DA:515,9 -DA:518,0 -DA:521,0 -DA:523,0 -DA:524,0 -DA:525,0 -DA:526,0 -DA:527,0 -DA:531,0 -DA:532,0 -DA:533,0 -DA:534,0 -DA:535,0 -DA:536,0 -DA:538,0 -DA:539,0 -DA:543,0 -DA:544,0 -DA:548,0 -DA:550,0 -LF:221 -LH:122 -end_of_record -SF:lib/src/service/scroll_service.dart -DA:21,9 -DA:24,9 -DA:28,9 -DA:29,9 -DA:39,9 -DA:40,27 -DA:42,1 -DA:44,3 -DA:45,2 -DA:48,1 -DA:49,3 -DA:51,1 -DA:52,3 -DA:54,1 -DA:56,1 -DA:57,3 -DA:58,3 -DA:63,9 -DA:65,9 -DA:66,9 -DA:67,9 -DA:68,9 -DA:70,9 -DA:71,18 -DA:76,5 -DA:78,15 -DA:79,5 -DA:80,15 -DA:81,15 -DA:86,1 -DA:88,1 -DA:89,4 -DA:92,1 -DA:94,1 -DA:95,4 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -LF:39 -LH:35 -end_of_record -SF:lib/src/service/keyboard_service.dart -DA:19,9 -DA:24,9 -DA:30,9 -DA:31,9 -DA:40,9 -DA:42,9 -DA:43,9 -DA:44,9 -DA:45,9 -DA:46,18 -DA:50,9 -DA:52,18 -DA:54,9 -DA:57,1 -DA:59,1 -DA:60,2 -DA:63,0 -DA:65,0 -DA:66,0 -DA:69,8 -DA:71,24 -DA:73,8 -DA:77,24 -DA:80,24 -DA:83,8 -DA:85,7 -DA:87,7 -DA:95,1 -DA:96,3 -DA:99,0 -DA:100,0 -DA:104,0 -LF:32 -LH:26 -end_of_record -SF:lib/src/service/input_service.dart -DA:18,9 -DA:22,9 -DA:27,9 -DA:28,9 -DA:36,27 -DA:38,9 -DA:40,9 -DA:42,36 -DA:43,18 -DA:46,9 -DA:48,9 -DA:49,36 -DA:50,18 -DA:52,9 -DA:55,9 -DA:57,9 -DA:58,18 -DA:62,9 -DA:64,18 -DA:74,9 -DA:75,9 -DA:76,9 -DA:79,1 -DA:82,2 -DA:83,1 -DA:85,1 -DA:86,1 -DA:87,0 -DA:88,0 -DA:89,0 -DA:90,0 -DA:91,0 -DA:95,1 -DA:96,1 -DA:97,1 -DA:98,4 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:105,2 -DA:110,1 -DA:111,3 -DA:112,2 -DA:116,1 -DA:117,2 -DA:118,2 -DA:119,1 -DA:121,1 -DA:122,1 -DA:124,1 -DA:130,0 -DA:131,0 -DA:132,0 -DA:136,0 -DA:137,0 -DA:138,0 -DA:139,0 -DA:140,0 -DA:141,0 -DA:147,0 -DA:148,0 -DA:149,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:165,9 -DA:167,18 -DA:168,9 -DA:171,0 -DA:176,0 -DA:178,0 -DA:180,0 -DA:182,0 -DA:184,0 -DA:189,0 -DA:194,0 -DA:199,0 -DA:204,0 -DA:209,0 -DA:214,0 -DA:219,0 -DA:221,0 -DA:223,0 -DA:226,0 -DA:231,9 -DA:232,36 -DA:233,9 -DA:235,45 -DA:237,9 -DA:238,9 -DA:239,27 -DA:240,9 -DA:241,9 -DA:243,9 -DA:244,18 -DA:245,18 -DA:247,9 -DA:250,18 -DA:251,16 -DA:260,8 -DA:261,8 -DA:264,7 -DA:265,7 -DA:267,7 -DA:268,7 -DA:269,14 -DA:271,7 -DA:272,7 -DA:273,7 -LF:114 -LH:70 -end_of_record -SF:lib/src/document/node_iterator.dart -DA:14,5 -DA:18,5 -DA:20,5 -DA:21,10 -DA:22,5 -DA:26,5 -DA:31,15 -DA:32,5 -DA:36,10 -DA:37,0 -DA:38,5 -DA:39,10 -DA:41,0 -DA:42,0 -DA:44,0 -DA:46,0 -DA:50,5 -DA:53,0 -DA:54,0 -DA:55,0 -DA:60,5 -DA:62,5 -DA:65,5 -DA:66,5 -DA:68,5 -DA:69,10 -LF:26 -LH:18 -end_of_record -SF:lib/src/extensions/path_extensions.dart -DA:6,8 -DA:7,22 -DA:8,16 -DA:9,24 -DA:16,6 -DA:17,18 -DA:18,12 -DA:19,18 -DA:26,1 -DA:27,1 -DA:28,1 -DA:31,1 -DA:33,1 -DA:34,2 -LF:14 -LH:14 -end_of_record -SF:lib/src/undo_manager.dart -DA:19,7 -DA:26,0 -DA:27,0 -DA:30,10 -DA:32,0 -DA:33,0 -DA:36,7 -DA:37,14 -DA:41,1 -DA:42,1 -DA:43,5 -DA:44,2 -DA:45,1 -DA:46,1 -DA:48,2 -DA:49,2 -DA:50,1 -DA:58,10 -DA:60,7 -DA:61,28 -DA:62,0 -DA:64,14 -DA:67,1 -DA:68,2 -DA:71,2 -DA:73,2 -DA:78,0 -DA:79,0 -DA:82,15 -DA:84,21 -DA:86,0 -DA:94,10 -DA:95,10 -DA:96,10 -DA:98,7 -DA:99,14 -DA:100,7 -DA:101,14 -DA:104,10 -DA:105,5 -DA:106,0 -DA:107,0 -DA:108,0 -DA:114,1 -DA:115,2 -DA:116,1 -DA:120,2 -DA:124,1 -DA:125,1 -DA:133,1 -DA:134,2 -DA:135,1 -DA:139,2 -DA:143,1 -DA:144,1 -LF:55 -LH:44 -end_of_record -SF:lib/src/extensions/color_extension.dart -DA:6,0 -DA:7,0 -DA:8,0 -DA:13,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:24,0 -DA:30,0 -DA:33,0 -DA:34,0 -LF:15 -LH:0 -end_of_record -SF:lib/src/extensions/node_extensions.dart -DA:9,9 -DA:10,36 -DA:12,3 -DA:13,36 -DA:15,0 -DA:16,0 -DA:17,0 -DA:19,0 -DA:23,9 -DA:24,9 -DA:25,18 -DA:26,27 -LF:12 -LH:8 -end_of_record -SF:lib/src/extensions/object_extensions.dart -DA:2,9 -DA:3,9 -LF:2 -LH:2 -end_of_record -SF:lib/src/extensions/text_node_extensions.dart -DA:9,1 -DA:10,1 -DA:12,1 -DA:13,1 -DA:15,1 -DA:16,1 -DA:18,1 -DA:19,1 -DA:21,2 -DA:22,4 -DA:24,6 -DA:26,6 -DA:28,4 -DA:29,2 -DA:32,2 -DA:33,6 -DA:34,2 -DA:35,4 -DA:36,6 -DA:40,2 -DA:45,1 -DA:46,2 -DA:48,3 -DA:50,3 -DA:52,2 -DA:53,1 -DA:56,1 -DA:57,3 -DA:58,1 -DA:59,2 -DA:60,3 -DA:64,1 -DA:71,0 -DA:72,0 -DA:74,0 -DA:75,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:81,0 -DA:83,1 -DA:84,1 -DA:87,2 -DA:88,2 -DA:90,3 -DA:91,1 -DA:93,5 -DA:94,1 -DA:95,4 -DA:97,3 -DA:98,4 -DA:99,1 -DA:100,2 -DA:103,1 -DA:104,2 -DA:105,4 -DA:108,1 -LF:57 -LH:49 -end_of_record -SF:lib/src/render/rich_text/rich_text_style.dart -DA:46,0 -DA:53,3 -DA:69,6 -DA:70,2 -DA:71,2 -DA:72,2 -DA:73,2 -DA:74,2 -DA:79,2 -DA:80,6 -DA:81,2 -DA:86,2 -DA:87,2 -DA:88,6 -DA:93,0 -DA:94,0 -DA:97,0 -DA:98,0 -DA:104,1 -DA:105,3 -DA:106,1 -DA:111,9 -DA:112,9 -DA:113,0 -DA:118,4 -DA:119,12 -DA:120,4 -DA:127,9 -DA:128,13 -DA:131,9 -DA:132,13 -DA:135,9 -DA:136,9 -DA:137,4 -DA:140,9 -DA:141,9 -DA:142,4 -DA:145,9 -DA:146,13 -DA:147,2 -DA:148,4 -DA:154,9 -DA:155,9 -DA:156,0 -DA:157,0 -DA:158,0 -DA:164,0 -DA:169,9 -DA:170,9 -DA:171,0 -DA:179,9 -DA:185,4 -DA:186,12 -DA:192,27 -DA:194,4 -DA:198,9 -DA:199,9 -DA:200,9 -DA:201,9 -DA:202,9 -DA:203,9 -DA:204,9 -DA:205,9 -DA:206,9 -DA:207,9 -DA:210,9 -DA:214,9 -DA:215,9 -DA:216,0 -DA:217,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:226,9 -DA:227,18 -DA:234,9 -DA:235,9 -DA:236,34 -DA:237,2 -DA:240,18 -DA:241,2 -DA:243,9 -DA:247,9 -DA:248,18 -DA:251,9 -DA:252,18 -DA:255,18 -DA:258,9 -DA:259,18 -DA:260,0 -DA:261,18 -DA:262,0 -DA:268,9 -DA:273,9 -DA:274,18 -DA:276,0 -DA:277,0 -LF:97 -LH:77 -end_of_record -SF:lib/src/infra/flowy_svg.dart -DA:5,8 -DA:12,8 -DA:20,8 -DA:22,8 -DA:23,8 -DA:24,8 -DA:28,8 -DA:29,8 -DA:30,8 -DA:31,8 -DA:32,8 -DA:33,16 -DA:34,8 -DA:39,1 -DA:41,2 -DA:42,1 -DA:44,2 -DA:45,2 -DA:48,0 -LF:19 -LH:18 -end_of_record -SF:lib/src/infra/html_converter.dart -DA:33,0 -DA:34,0 -DA:35,0 -DA:36,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:54,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:77,0 -DA:78,0 -DA:80,0 -DA:82,0 -DA:83,0 -DA:85,0 -DA:88,0 -DA:91,0 -DA:92,0 -DA:97,0 -DA:98,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:114,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:124,0 -DA:125,0 -DA:126,0 -DA:127,0 -DA:128,0 -DA:129,0 -DA:130,0 -DA:131,0 -DA:133,0 -DA:134,0 -DA:135,0 -DA:136,0 -DA:139,0 -DA:142,0 -DA:144,0 -DA:145,0 -DA:146,0 -DA:150,0 -DA:151,0 -DA:156,0 -DA:157,0 -DA:158,0 -DA:159,0 -DA:162,0 -DA:168,0 -DA:170,0 -DA:171,0 -DA:172,0 -DA:174,0 -DA:176,0 -DA:177,0 -DA:179,0 -DA:180,0 -DA:181,0 -DA:186,0 -DA:188,0 -DA:191,0 -DA:194,0 -DA:196,0 -DA:197,0 -DA:200,0 -DA:201,0 -DA:204,0 -DA:207,0 -DA:208,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:212,0 -DA:213,0 -DA:218,0 -DA:219,0 -DA:220,0 -DA:221,0 -DA:222,0 -DA:223,0 -DA:226,0 -DA:228,0 -DA:229,0 -DA:230,0 -DA:231,0 -DA:232,0 -DA:233,0 -DA:234,0 -DA:235,0 -DA:236,0 -DA:237,0 -DA:239,0 -DA:247,0 -DA:249,0 -DA:251,0 -DA:254,0 -DA:257,0 -DA:259,0 -DA:260,0 -DA:263,0 -DA:265,0 -DA:266,0 -DA:267,0 -DA:269,0 -DA:274,0 -DA:276,0 -DA:277,0 -DA:282,0 -DA:283,0 -DA:284,0 -DA:286,0 -DA:288,0 -DA:291,0 -DA:292,0 -DA:293,0 -DA:294,0 -DA:295,0 -DA:300,0 -DA:301,0 -DA:302,0 -DA:303,0 -DA:304,0 -DA:305,0 -DA:310,0 -DA:314,0 -DA:315,0 -DA:316,0 -DA:318,0 -DA:322,0 -DA:324,0 -DA:325,0 -DA:326,0 -DA:327,0 -DA:328,0 -DA:354,0 -DA:356,0 -DA:358,0 -DA:359,0 -DA:360,0 -DA:361,0 -DA:362,0 -DA:365,0 -DA:366,0 -DA:367,0 -DA:368,0 -DA:370,0 -DA:371,0 -DA:372,0 -DA:377,0 -DA:378,0 -DA:379,0 -DA:381,0 -DA:382,0 -DA:383,0 -DA:384,0 -DA:386,0 -DA:391,0 -DA:392,0 -DA:393,0 -DA:395,0 -DA:398,0 -DA:399,0 -DA:402,0 -DA:403,0 -DA:404,0 -DA:405,0 -DA:407,0 -DA:409,0 -DA:410,0 -DA:411,0 -DA:413,0 -DA:417,0 -DA:418,0 -DA:419,0 -DA:420,0 -DA:424,0 -DA:425,0 -DA:426,0 -DA:427,0 -DA:431,0 -DA:434,0 -DA:435,0 -DA:436,0 -DA:437,0 -DA:439,0 -DA:440,0 -DA:443,0 -DA:446,0 -DA:447,0 -DA:448,0 -DA:449,0 -DA:450,0 -DA:452,0 -DA:454,0 -DA:455,0 -DA:456,0 -DA:458,0 -DA:460,0 -DA:461,0 -DA:464,0 -DA:465,0 -DA:466,0 -DA:469,0 -DA:470,0 -DA:472,0 -DA:475,0 -DA:476,0 -DA:477,0 -DA:478,0 -DA:481,0 -DA:501,0 -DA:504,0 -DA:507,0 -DA:510,0 -DA:512,0 -DA:513,0 -DA:515,0 -DA:517,0 -DA:518,0 -DA:519,0 -DA:521,0 -DA:523,0 -DA:526,0 -DA:530,0 -DA:531,0 -DA:532,0 -DA:534,0 -DA:535,0 -DA:536,0 -DA:537,0 -DA:538,0 -DA:539,0 -DA:540,0 -DA:541,0 -DA:542,0 -DA:543,0 -DA:544,0 -DA:545,0 -DA:546,0 -DA:547,0 -DA:548,0 -DA:549,0 -DA:550,0 -DA:551,0 -DA:552,0 -DA:554,0 -DA:555,0 -DA:556,0 -DA:557,0 -DA:559,0 -DA:560,0 -DA:563,0 -DA:568,0 -DA:569,0 -DA:570,0 -DA:571,0 -DA:573,0 -DA:574,0 -DA:576,0 -DA:577,0 -DA:578,0 -DA:579,0 -DA:581,0 -DA:582,0 -DA:585,0 -DA:586,0 -DA:587,0 -DA:594,0 -DA:595,0 -DA:596,0 -DA:599,0 -DA:600,0 -LF:299 -LH:0 -end_of_record -SF:lib/src/render/editor/editor_entry.dart -DA:8,9 -DA:10,9 -DA:11,18 -DA:12,9 -DA:13,9 -DA:17,9 -DA:18,9 -DA:19,18 -DA:24,9 -DA:28,9 -DA:33,9 -DA:35,9 -DA:37,18 -DA:38,9 -DA:39,9 -DA:40,36 -DA:41,9 -DA:42,9 -DA:45,9 -DA:47,0 -DA:50,0 -DA:54,9 -LF:22 -LH:20 -end_of_record -SF:lib/src/render/rich_text/bulleted_list_text.dart -DA:12,3 -DA:14,3 -DA:15,6 -DA:16,3 -DA:17,3 -DA:21,3 -DA:22,3 -DA:28,3 -DA:32,3 -DA:37,3 -DA:39,3 -DA:53,3 -DA:55,6 -DA:57,3 -DA:59,12 -DA:61,3 -DA:63,3 -DA:64,3 -DA:65,3 -DA:67,3 -DA:68,3 -DA:69,3 -DA:70,6 -DA:72,6 -DA:75,3 -DA:76,3 -DA:77,3 -DA:79,6 -DA:80,6 -LF:29 -LH:29 -end_of_record -SF:lib/src/render/rich_text/default_selectable.dart -DA:11,18 -DA:12,18 -DA:13,30 -DA:14,10 -DA:15,30 -DA:21,0 -DA:22,0 -DA:24,18 -DA:25,72 -DA:27,10 -DA:28,5 -DA:29,20 -DA:30,5 -DA:32,0 -DA:33,0 -DA:35,60 -DA:37,0 -DA:38,0 -DA:40,15 -DA:42,15 -LF:20 -LH:14 -end_of_record -SF:lib/src/render/rich_text/flowy_rich_text.dart -DA:19,9 -DA:28,9 -DA:38,9 -DA:39,9 -DA:48,9 -DA:49,27 -DA:51,2 -DA:52,6 -DA:54,9 -DA:56,9 -DA:59,5 -DA:60,20 -DA:62,5 -DA:63,5 -DA:64,35 -DA:66,7 -DA:68,14 -DA:70,14 -DA:71,14 -DA:72,14 -DA:73,4 -DA:76,7 -DA:77,35 -DA:78,7 -DA:79,14 -DA:85,0 -DA:87,0 -DA:88,0 -DA:89,0 -DA:92,0 -DA:94,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:102,5 -DA:104,25 -DA:105,30 -DA:107,5 -DA:108,10 -DA:109,10 -DA:111,5 -DA:112,5 -DA:113,15 -DA:114,5 -DA:117,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:124,0 -DA:130,9 -DA:131,9 -DA:133,36 -DA:134,2 -DA:135,2 -DA:136,2 -DA:137,2 -DA:140,9 -DA:144,2 -DA:145,2 -DA:146,2 -DA:147,2 -DA:150,4 -DA:151,6 -DA:156,9 -DA:157,9 -DA:158,9 -DA:159,9 -DA:162,18 -DA:163,12 -DA:169,0 -DA:170,0 -DA:172,0 -DA:173,0 -DA:174,0 -DA:175,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:181,0 -DA:185,0 -DA:190,9 -DA:192,18 -DA:195,18 -DA:196,27 -DA:197,9 -DA:198,27 -DA:199,17 -DA:200,9 -DA:201,9 -DA:202,9 -DA:203,9 -DA:206,6 -DA:207,2 -DA:208,4 -DA:209,2 -DA:212,2 -DA:213,2 -LF:101 -LH:72 -end_of_record -SF:lib/src/render/rich_text/checkbox_text.dart -DA:13,4 -DA:15,4 -DA:16,8 -DA:17,4 -DA:18,4 -DA:22,4 -DA:23,4 -DA:24,8 -DA:29,4 -DA:33,4 -DA:38,4 -DA:39,4 -DA:51,4 -DA:53,8 -DA:55,4 -DA:57,16 -DA:58,4 -DA:60,0 -DA:64,4 -DA:65,16 -DA:66,16 -DA:67,4 -DA:69,4 -DA:70,4 -DA:71,4 -DA:73,4 -DA:74,4 -DA:75,4 -DA:76,4 -DA:77,8 -DA:78,4 -DA:79,4 -DA:82,1 -DA:83,2 -DA:84,3 -DA:85,4 -DA:88,1 -DA:91,4 -DA:92,4 -DA:93,4 -DA:95,8 -DA:96,4 -DA:97,4 -DA:98,8 -DA:106,0 -DA:107,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:116,0 -DA:117,0 -DA:118,0 -DA:119,0 -DA:120,0 -DA:121,0 -DA:122,0 -DA:125,0 -DA:127,0 -DA:130,0 -DA:134,0 -DA:142,4 -DA:143,4 -DA:144,4 -DA:145,4 -DA:146,4 -DA:147,8 -DA:148,4 -DA:149,16 -DA:150,8 -DA:152,8 -DA:154,8 -DA:155,12 -DA:158,3 -DA:159,4 -DA:162,4 -LF:76 -LH:58 -end_of_record -SF:lib/src/render/rich_text/heading_text.dart -DA:11,2 -DA:13,2 -DA:14,4 -DA:15,2 -DA:16,2 -DA:20,2 -DA:21,2 -DA:22,4 -DA:27,2 -DA:31,2 -DA:36,2 -DA:37,2 -DA:44,0 -DA:50,2 -DA:52,4 -DA:54,2 -DA:56,4 -DA:59,2 -DA:61,2 -DA:62,2 -DA:63,2 -DA:66,2 -DA:68,2 -DA:69,2 -DA:71,2 -DA:72,2 -DA:73,4 -DA:74,4 -DA:80,2 -DA:81,2 -DA:82,2 -DA:83,2 -DA:84,2 -DA:85,4 -DA:86,2 -DA:87,4 -DA:88,8 -DA:90,2 -DA:93,2 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:107,0 -DA:110,0 -LF:50 -LH:38 -end_of_record -SF:lib/src/render/rich_text/number_list_text.dart -DA:12,1 -DA:14,1 -DA:15,2 -DA:16,1 -DA:17,1 -DA:21,1 -DA:22,1 -DA:23,2 -DA:28,1 -DA:32,1 -DA:37,1 -DA:39,1 -DA:53,1 -DA:55,2 -DA:57,1 -DA:59,4 -DA:60,1 -DA:61,1 -DA:62,1 -DA:64,1 -DA:66,1 -DA:67,1 -DA:68,1 -DA:69,2 -DA:71,2 -DA:72,4 -DA:74,1 -DA:75,1 -DA:76,1 -DA:78,2 -DA:79,2 -LF:31 -LH:31 -end_of_record -SF:lib/src/render/rich_text/quoted_text.dart -DA:12,2 -DA:14,2 -DA:15,4 -DA:16,2 -DA:17,2 -DA:21,2 -DA:22,2 -DA:28,2 -DA:32,2 -DA:37,2 -DA:38,2 -DA:52,2 -DA:54,4 -DA:56,2 -DA:58,8 -DA:59,2 -DA:61,2 -DA:62,2 -DA:63,2 -DA:65,2 -DA:66,2 -DA:67,2 -DA:68,6 -DA:70,4 -DA:73,2 -DA:74,2 -DA:75,2 -DA:77,4 -DA:78,4 -DA:86,2 -DA:88,16 -DA:89,6 -LF:32 -LH:32 -end_of_record -SF:lib/src/render/rich_text/rich_text.dart -DA:12,8 -DA:14,8 -DA:15,16 -DA:16,8 -DA:17,8 -DA:21,8 -DA:22,8 -DA:28,8 -DA:32,8 -DA:37,8 -DA:38,8 -DA:45,8 -DA:50,8 -DA:52,16 -DA:54,8 -DA:56,8 -DA:58,8 -DA:59,8 -DA:60,8 -DA:61,8 -DA:62,16 -DA:63,16 -LF:22 -LH:22 -end_of_record -SF:lib/src/render/selection/cursor_widget.dart -DA:6,7 -DA:12,7 -DA:19,7 -DA:20,7 -DA:27,7 -DA:29,7 -DA:31,14 -DA:34,7 -DA:36,14 -DA:37,7 -DA:40,7 -DA:41,7 -DA:42,35 -DA:43,2 -DA:44,4 -DA:45,4 -DA:51,4 -DA:52,8 -DA:53,4 -DA:55,8 -DA:56,8 -DA:59,7 -DA:61,7 -DA:62,14 -DA:63,7 -DA:64,14 -DA:65,21 -DA:69,7 -DA:70,7 -DA:71,21 -LF:30 -LH:30 -end_of_record -SF:lib/src/render/selection/selection_widget.dart -DA:4,5 -DA:9,5 -DA:15,5 -DA:16,5 -DA:20,5 -DA:22,5 -DA:23,10 -DA:24,5 -DA:25,10 -DA:26,15 -DA:30,5 -DA:31,5 -DA:32,10 -LF:13 -LH:13 -end_of_record -SF:lib/src/render/selection/toolbar_widget.dart -DA:12,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:26,0 -DA:38,5 -DA:44,5 -DA:51,5 -DA:52,5 -DA:68,5 -DA:70,5 -DA:71,15 -DA:72,15 -DA:73,5 -DA:74,10 -DA:76,10 -DA:77,5 -DA:82,5 -DA:84,5 -DA:85,5 -DA:88,5 -DA:89,5 -DA:90,10 -DA:92,5 -DA:93,5 -DA:94,5 -DA:96,5 -DA:97,5 -DA:98,5 -DA:99,5 -DA:100,5 -DA:101,5 -DA:102,5 -DA:103,5 -DA:104,5 -DA:105,5 -DA:106,5 -DA:113,5 -DA:114,5 -DA:116,5 -DA:117,5 -DA:118,0 -DA:122,5 -DA:124,5 -DA:128,5 -DA:129,0 -DA:130,5 -DA:132,10 -DA:133,10 -DA:134,5 -DA:135,5 -DA:136,5 -DA:144,0 -DA:146,0 -DA:148,0 -DA:150,0 -DA:151,0 -DA:152,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:164,0 -DA:165,0 -DA:166,0 -DA:169,0 -DA:170,0 -DA:171,0 -DA:177,0 -DA:179,0 -DA:180,0 -DA:187,0 -DA:188,0 -DA:198,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:206,0 -LF:89 -LH:45 -end_of_record -SF:lib/src/service/default_text_operations/format_rich_text_style.dart -DA:11,0 -DA:12,0 -DA:18,0 -DA:19,0 -DA:24,0 -DA:25,0 -DA:31,0 -DA:32,0 -DA:37,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:45,0 -DA:46,0 -DA:47,0 -DA:49,0 -DA:50,0 -DA:52,0 -DA:54,0 -DA:56,0 -DA:57,0 -DA:59,0 -DA:65,0 -DA:66,0 -DA:69,0 -DA:70,0 -DA:76,0 -DA:77,0 -DA:82,0 -DA:83,0 -DA:89,0 -DA:90,0 -DA:95,0 -DA:96,0 -DA:97,0 -DA:99,0 -DA:103,0 -DA:105,0 -DA:107,0 -DA:109,0 -DA:110,0 -DA:111,0 -DA:112,0 -DA:114,0 -DA:115,0 -DA:116,0 -DA:117,0 -DA:122,0 -DA:126,1 -DA:127,1 -DA:130,1 -DA:131,1 -DA:134,1 -DA:135,1 -DA:138,1 -DA:139,1 -DA:142,1 -DA:143,4 -DA:144,3 -DA:145,2 -DA:147,1 -DA:151,1 -DA:152,1 -DA:156,1 -DA:159,1 -DA:160,4 -DA:161,3 -DA:167,1 -DA:168,1 -DA:170,2 -DA:171,1 -DA:175,1 -DA:180,5 -DA:181,1 -DA:182,1 -DA:183,2 -DA:184,5 -DA:188,3 -DA:189,1 -DA:191,2 -DA:192,3 -DA:193,2 -DA:194,5 -DA:195,5 -DA:196,2 -DA:198,1 -DA:207,1 -LF:87 -LH:39 -end_of_record -SF:lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart -DA:13,27 -DA:14,9 -DA:15,9 -DA:16,9 -DA:17,9 -DA:18,9 -DA:19,9 -DA:20,9 -DA:21,9 -DA:22,9 -DA:23,9 -LF:11 -LH:11 -end_of_record -SF:lib/src/service/toolbar_service.dart -DA:16,9 -DA:20,9 -DA:25,9 -DA:26,9 -DA:34,5 -DA:36,5 -DA:38,10 -DA:39,10 -DA:40,5 -DA:41,10 -DA:43,10 -DA:47,20 -DA:50,9 -DA:52,28 -DA:53,14 -DA:54,9 -DA:57,9 -DA:59,9 -DA:60,18 -DA:64,9 -DA:66,9 -DA:68,9 -LF:22 -LH:22 -end_of_record -SF:lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart -DA:5,0 -DA:6,0 -DA:7,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:16,0 -DA:18,0 -DA:19,0 -DA:24,0 -DA:25,0 -DA:27,0 -DA:31,0 -DA:32,0 -DA:33,0 -DA:34,0 -DA:35,0 -DA:37,0 -DA:42,0 -DA:43,0 -DA:45,0 -DA:50,0 -DA:51,0 -DA:52,0 -DA:55,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:61,0 -DA:62,0 -DA:63,0 -DA:66,0 -DA:67,0 -DA:68,0 -DA:69,0 -DA:72,2 -DA:73,2 -DA:78,4 -DA:79,0 -DA:80,0 -DA:82,0 -DA:84,4 -DA:85,0 -DA:86,0 -DA:88,0 -DA:90,4 -DA:91,0 -DA:92,0 -DA:94,0 -DA:96,4 -DA:97,0 -DA:98,0 -DA:100,0 -DA:106,24 -DA:107,6 -DA:108,2 -DA:111,6 -DA:116,10 -DA:117,0 -DA:118,0 -DA:120,0 -DA:124,0 -DA:127,10 -DA:128,0 -DA:129,0 -DA:131,0 -DA:134,0 -DA:137,10 -DA:138,0 -DA:139,0 -DA:140,0 -DA:142,10 -DA:143,0 -DA:144,0 -DA:145,0 -LF:75 -LH:14 -end_of_record -SF:lib/src/service/internal_key_event_handlers/copy_paste_handler.dart -DA:8,0 -DA:9,0 -DA:10,0 -DA:13,0 -DA:14,0 -DA:15,0 -DA:17,0 -DA:18,0 -DA:19,0 -DA:20,0 -DA:21,0 -DA:22,0 -DA:23,0 -DA:25,0 -DA:30,0 -DA:31,0 -DA:33,0 -DA:35,0 -DA:37,0 -DA:38,0 -DA:39,0 -DA:40,0 -DA:41,0 -DA:44,0 -DA:45,0 -DA:50,0 -DA:52,0 -DA:53,0 -DA:57,0 -DA:58,0 -DA:60,0 -DA:62,0 -DA:63,0 -DA:64,0 -DA:65,0 -DA:66,0 -DA:67,0 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:79,0 -DA:82,0 -DA:84,0 -DA:86,0 -DA:87,0 -DA:89,0 -DA:93,0 -DA:95,0 -DA:97,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:103,0 -DA:104,0 -DA:105,0 -DA:106,0 -DA:107,0 -DA:108,0 -DA:109,0 -DA:110,0 -DA:113,0 -DA:116,0 -DA:117,0 -DA:121,0 -DA:122,0 -DA:123,0 -DA:126,0 -DA:127,0 -DA:129,0 -DA:130,0 -DA:134,0 -DA:136,0 -DA:137,0 -DA:141,0 -DA:142,0 -DA:143,0 -DA:146,0 -DA:147,0 -DA:152,0 -DA:153,0 -DA:154,0 -DA:155,0 -DA:156,0 -DA:158,0 -DA:159,0 -DA:160,0 -DA:161,0 -DA:162,0 -DA:163,0 -DA:168,0 -DA:169,0 -DA:170,0 -DA:172,0 -DA:176,0 -DA:177,0 -DA:178,0 -DA:180,0 -DA:181,0 -DA:182,0 -DA:185,0 -DA:186,0 -DA:192,0 -DA:193,0 -DA:199,0 -DA:200,0 -DA:201,0 -DA:203,0 -DA:205,0 -DA:207,0 -DA:209,0 -DA:210,0 -DA:211,0 -DA:213,0 -DA:214,0 -DA:219,0 -DA:220,0 -DA:222,0 -DA:224,0 -DA:225,0 -DA:226,0 -DA:227,0 -DA:229,0 -DA:231,0 -DA:232,0 -DA:234,0 -DA:236,0 -DA:237,0 -DA:238,0 -DA:239,0 -DA:241,0 -DA:242,0 -DA:245,0 -DA:246,0 -DA:252,0 -DA:253,0 -DA:254,0 -DA:255,0 -DA:258,0 -DA:259,0 -DA:260,0 -DA:263,0 -DA:264,0 -DA:265,0 -DA:266,0 -DA:268,0 -DA:269,0 -DA:270,0 -DA:272,0 -DA:273,0 -DA:274,0 -DA:275,0 -DA:276,0 -DA:279,0 -DA:281,0 -DA:282,0 -DA:283,0 -DA:284,0 -DA:286,0 -DA:287,0 -DA:288,0 -DA:289,0 -DA:290,0 -DA:292,0 -DA:293,0 -DA:294,0 -DA:300,0 -DA:303,0 -DA:304,0 -DA:307,24 -DA:308,12 -DA:309,0 -DA:312,12 -DA:313,0 -DA:316,12 -DA:317,0 -LF:178 -LH:4 -end_of_record -SF:lib/src/service/internal_key_event_handlers/delete_text_handler.dart -DA:6,2 -DA:7,8 -DA:11,6 -DA:12,6 -DA:13,4 -DA:15,4 -DA:16,6 -DA:20,2 -DA:21,4 -DA:22,1 -DA:23,4 -DA:24,2 -DA:26,1 -DA:28,2 -DA:31,2 -DA:32,1 -DA:33,1 -DA:40,1 -DA:41,2 -DA:42,1 -DA:44,1 -DA:45,1 -DA:46,2 -DA:47,1 -DA:48,1 -DA:49,2 -DA:57,1 -DA:58,1 -DA:61,3 -DA:64,1 -DA:66,2 -DA:67,5 -DA:72,2 -DA:75,4 -DA:76,2 -DA:82,1 -DA:83,4 -DA:87,3 -DA:88,3 -DA:89,2 -DA:91,2 -DA:92,3 -DA:96,1 -DA:97,2 -DA:98,1 -DA:99,5 -DA:100,2 -DA:101,1 -DA:105,1 -DA:106,1 -DA:108,1 -DA:110,4 -DA:111,1 -DA:112,1 -DA:114,2 -DA:115,3 -DA:118,1 -DA:120,2 -DA:121,5 -DA:126,1 -DA:129,1 -DA:134,2 -DA:136,2 -DA:137,2 -DA:138,4 -DA:139,8 -DA:143,4 -DA:144,2 -DA:147,4 -DA:148,4 -DA:153,26 -DA:154,16 -DA:155,2 -DA:157,16 -DA:158,1 -LF:75 -LH:75 -end_of_record -SF:lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart -DA:21,18 -DA:22,5 -DA:23,11 -DA:27,4 -DA:28,3 -DA:32,1 -DA:33,1 -DA:34,2 -DA:36,2 -DA:38,3 -DA:43,1 -DA:44,1 -DA:46,4 -DA:47,1 -DA:48,4 -DA:50,1 -DA:51,1 -DA:52,1 -DA:53,2 -DA:54,3 -DA:56,1 -DA:57,1 -DA:58,1 -DA:60,2 -DA:62,1 -DA:63,1 -DA:68,2 -DA:72,1 -DA:76,4 -DA:77,3 -DA:78,1 -DA:79,2 -DA:81,1 -DA:82,1 -DA:84,1 -DA:85,1 -DA:86,1 -DA:88,1 -DA:89,1 -DA:91,1 -DA:92,3 -DA:94,1 -DA:95,1 -DA:96,1 -DA:97,1 -DA:99,1 -DA:100,1 -DA:107,1 -DA:108,3 -DA:109,2 -DA:110,1 -DA:112,2 -DA:113,1 -DA:114,1 -DA:117,1 -DA:118,3 -DA:120,1 -DA:121,1 -DA:122,2 -DA:123,1 -DA:125,4 -DA:128,1 -DA:130,2 -DA:131,5 -DA:133,1 -DA:134,1 -LF:66 -LH:66 -end_of_record -SF:lib/src/service/internal_key_event_handlers/redo_undo_handler.dart -DA:5,24 -DA:6,12 -DA:7,1 -DA:8,2 -DA:10,2 -LF:5 -LH:5 -end_of_record -SF:lib/src/service/internal_key_event_handlers/slash_handler.dart -DA:14,1 -DA:15,1 -DA:17,3 -DA:18,1 -DA:20,1 -DA:21,1 -DA:22,0 -DA:23,0 -DA:26,1 -DA:28,1 -DA:29,1 -DA:30,0 -DA:31,0 -DA:33,1 -DA:35,1 -DA:36,1 -DA:37,0 -DA:38,0 -DA:40,1 -DA:42,1 -DA:43,1 -DA:44,0 -DA:45,0 -DA:47,1 -DA:49,1 -DA:50,1 -DA:51,0 -DA:59,1 -DA:61,1 -DA:62,1 -DA:63,0 -DA:70,25 -DA:71,14 -DA:75,3 -DA:76,1 -DA:77,2 -DA:81,4 -DA:82,1 -DA:83,1 -DA:84,1 -DA:88,3 -DA:89,1 -DA:92,1 -DA:93,3 -DA:94,6 -DA:95,1 -DA:98,3 -DA:101,3 -DA:102,1 -DA:103,3 -DA:104,1 -DA:106,3 -DA:108,3 -DA:114,1 -DA:116,0 -DA:117,1 -DA:118,2 -DA:119,1 -DA:120,1 -DA:121,1 -DA:123,1 -DA:128,2 -DA:131,1 -DA:136,3 -DA:139,4 -DA:148,1 -DA:151,3 -DA:152,3 -DA:157,1 -DA:162,1 -DA:168,1 -DA:169,1 -DA:180,0 -DA:181,0 -DA:182,0 -DA:184,0 -DA:185,0 -DA:186,0 -DA:187,0 -DA:188,0 -DA:190,0 -DA:191,0 -DA:192,0 -DA:195,0 -DA:198,0 -DA:199,0 -DA:201,0 -DA:202,0 -DA:203,0 -DA:208,1 -DA:210,1 -DA:212,3 -DA:214,3 -DA:215,2 -DA:219,1 -DA:221,2 -DA:223,1 -DA:226,1 -DA:228,1 -DA:229,1 -DA:230,1 -DA:231,1 -DA:232,1 -DA:234,1 -DA:235,1 -DA:238,1 -DA:241,1 -DA:243,2 -DA:244,0 -DA:245,1 -DA:247,3 -DA:253,0 -DA:268,1 -DA:269,1 -DA:270,1 -DA:271,3 -DA:272,5 -DA:273,2 -DA:277,1 -DA:279,2 -DA:280,2 -DA:281,1 -DA:282,1 -DA:285,1 -DA:286,2 -DA:290,1 -DA:295,0 -DA:296,0 -DA:297,0 -DA:301,0 -DA:308,0 -DA:309,0 -DA:310,0 -DA:311,0 -DA:314,0 -DA:315,0 -DA:317,0 -DA:318,0 -DA:319,0 -DA:321,0 -DA:323,0 -DA:325,0 -DA:326,0 -DA:327,0 -DA:328,0 -DA:332,0 -DA:333,0 -DA:334,0 -DA:335,0 -DA:336,0 -DA:337,0 -DA:338,0 -DA:339,0 -DA:340,0 -DA:342,0 -DA:343,0 -DA:344,0 -DA:351,0 -DA:353,0 -DA:355,0 -DA:356,0 -DA:358,0 -DA:359,0 -DA:360,0 -DA:361,0 -DA:364,0 -DA:368,0 -DA:370,0 -DA:372,0 -DA:373,0 -DA:375,0 -DA:376,0 -DA:377,0 -DA:378,0 -DA:381,0 -DA:387,1 -DA:392,1 -DA:398,1 -DA:400,1 -DA:402,1 -DA:404,1 -DA:405,2 -DA:406,1 -DA:408,1 -DA:411,1 -DA:412,1 -DA:413,1 -DA:415,1 -DA:416,2 -DA:423,0 -DA:424,0 -DA:433,1 -DA:448,2 -DA:449,1 -LF:194 -LH:113 -end_of_record -SF:lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart -DA:8,22 -DA:9,4 -DA:13,8 -DA:14,6 -DA:15,4 -DA:17,1 -DA:21,2 -DA:22,1 -DA:24,2 -DA:25,1 -DA:27,2 -DA:28,1 -DA:30,2 -DA:31,1 -DA:32,1 -LF:15 -LH:15 -end_of_record -SF:lib/src/service/internal_key_event_handlers/whitespace_handler.dart -DA:12,1 -DA:14,1 -DA:16,1 -DA:23,21 -DA:24,6 -DA:32,4 -DA:33,1 -DA:37,3 -DA:38,1 -DA:39,2 -DA:43,1 -DA:44,1 -DA:45,3 -DA:46,1 -DA:47,2 -DA:48,1 -DA:49,2 -DA:50,1 -DA:56,1 -DA:57,2 -DA:60,1 -DA:61,1 -DA:62,2 -DA:65,2 -DA:66,1 -DA:67,1 -DA:71,1 -DA:75,1 -DA:76,2 -DA:81,1 -DA:82,3 -DA:83,1 -DA:84,1 -DA:87,1 -DA:88,3 -DA:89,1 -DA:93,1 -DA:94,2 -DA:95,2 -DA:99,2 -DA:100,1 -DA:101,1 -DA:105,1 -DA:109,1 -DA:111,1 -DA:112,1 -DA:115,1 -DA:116,3 -DA:119,1 -DA:120,1 -DA:121,2 -DA:125,2 -DA:126,1 -DA:127,1 -DA:131,1 -DA:135,1 -DA:136,2 -DA:137,5 -LF:58 -LH:58 -end_of_record -SF:lib/src/service/internal_key_event_handlers/select_all_handler.dart -DA:5,1 -DA:6,4 -DA:9,4 -DA:10,4 -DA:12,1 -DA:13,2 -DA:15,2 -DA:16,2 -DA:17,2 -DA:21,21 -DA:22,5 -DA:23,1 -LF:12 -LH:12 -end_of_record -SF:lib/src/service/internal_key_event_handlers/page_up_down_handler.dart -DA:5,20 -DA:6,4 -DA:7,3 -DA:8,2 -DA:10,3 -DA:13,4 -DA:14,3 -DA:15,2 -DA:17,3 -LF:9 -LH:9 -end_of_record -SF:lib/src/service/selection/selection_gesture.dart -DA:10,9 -DA:19,9 -DA:21,9 -DA:23,9 -DA:43,9 -DA:45,9 -DA:47,9 -DA:49,9 -DA:50,18 -DA:51,9 -DA:53,27 -DA:54,27 -DA:55,27 -DA:59,9 -DA:60,18 -DA:61,9 -DA:62,18 -DA:66,18 -DA:70,0 -DA:71,0 -DA:72,0 -DA:73,0 -DA:74,0 -DA:75,0 -DA:76,0 -DA:78,0 -DA:79,0 -DA:80,0 -DA:81,0 -DA:82,0 -DA:83,0 -DA:85,0 -DA:87,0 -DA:88,0 -DA:91,0 -DA:92,0 -DA:93,0 -DA:94,0 -DA:95,0 -DA:98,0 -DA:99,0 -DA:100,0 -DA:101,0 -DA:102,0 -DA:107,9 -DA:109,9 -DA:110,9 -DA:111,9 -LF:48 -LH:22 -end_of_record diff --git a/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json b/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json deleted file mode 100644 index 091adbfb6b..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/.vscode/launch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "example", - "request": "launch", - "type": "dart" - }, - { - "name": "example (profile mode)", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "example (release mode)", - "request": "launch", - "type": "dart", - "flutterMode": "release" - } - ] -} \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock b/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock deleted file mode 100644 index f86e34c312..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/example/pubspec.lock +++ /dev/null @@ -1,551 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.1" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.2" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.17.2" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - flowy_editor: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_inappwebview: - dependency: "direct main" - description: - name: flutter_inappwebview - url: "https://pub.dartlang.org" - source: hosted - version: "5.4.3+7" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - flutter_svg: - dependency: transitive - description: - name: flutter_svg - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1+1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - get: - dependency: transitive - description: - name: get - url: "https://pub.dartlang.org" - source: hosted - version: "4.6.5" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.15.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.1" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.4" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.6.0" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - lottie: - dependency: transitive - description: - name: lottie - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.1" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.4" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - nested: - dependency: transitive - description: - name: nested - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - path_drawing: - dependency: transitive - description: - name: path_drawing - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - path_parsing: - dependency: transitive - description: - name: path_parsing - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "5.0.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - pod_player: - dependency: "direct main" - description: - name: pod_player - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.8" - provider: - dependency: "direct main" - description: - name: provider - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.3" - rich_clipboard: - dependency: transitive - description: - name: rich_clipboard - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - rich_clipboard_android: - dependency: transitive - description: - name: rich_clipboard_android - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - rich_clipboard_ios: - dependency: transitive - description: - name: rich_clipboard_ios - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - rich_clipboard_linux: - dependency: transitive - description: - name: rich_clipboard_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - rich_clipboard_macos: - dependency: transitive - description: - name: rich_clipboard_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - rich_clipboard_platform_interface: - dependency: transitive - description: - name: rich_clipboard_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - rich_clipboard_web: - dependency: transitive - description: - name: rich_clipboard_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - rich_clipboard_windows: - dependency: transitive - description: - name: rich_clipboard_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.9" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - universal_html: - dependency: transitive - description: - name: universal_html - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.8" - universal_io: - dependency: transitive - description: - name: universal_io - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.5" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.17" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.17" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.12" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - video_player: - dependency: "direct main" - description: - name: video_player - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.5" - video_player_android: - dependency: transitive - description: - name: video_player_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.8" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.5" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "5.1.3" - video_player_web: - dependency: transitive - description: - name: video_player_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.12" - wakelock: - dependency: transitive - description: - name: wakelock - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.2" - wakelock_macos: - dependency: transitive - description: - name: wakelock_macos - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.0" - wakelock_platform_interface: - dependency: transitive - description: - name: wakelock_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" - wakelock_web: - dependency: transitive - description: - name: wakelock_web - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.0" - wakelock_windows: - dependency: transitive - description: - name: wakelock_windows - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "2.6.1" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.0" - youtube_explode_dart: - dependency: transitive - description: - name: youtube_explode_dart - url: "https://pub.dartlang.org" - source: hosted - version: "1.12.0" -sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 05c87f8e33..1f84c47ef6 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_editor -description: A new Flutter package project. +description: An easily extensible, test-covered rich text editing component for Flutter. version: 0.0.1 -homepage: +homepage: https://github.com/AppFlowy-IO/AppFlowy environment: sdk: ">=2.17.0 <3.0.0" From b7d71428be478c83a3f8c33d0f5829a4fbf476a8 Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 15:49:54 +0800 Subject: [PATCH 159/224] chore: send group notification --- .../grid/application/field/field_service.dart | 7 +- .../grid/application/row/row_service.dart | 24 +++-- .../flowy-grid/src/entities/grid_entities.rs | 94 +++++++++++++------ .../src/entities/group_entities/board_card.rs | 25 ++--- .../flowy-grid/src/entities/row_entities.rs | 8 +- .../src/entities/setting_entities.rs | 28 +++--- .../rust-lib/flowy-grid/src/event_handler.rs | 23 +++-- frontend/rust-lib/flowy-grid/src/event_map.rs | 16 ++-- .../flowy-grid/src/services/block_editor.rs | 6 +- .../flowy-grid/src/services/block_manager.rs | 12 ++- .../flowy-grid/src/services/grid_editor.rs | 29 +++--- .../src/services/grid_view_editor.rs | 79 ++++++++++++---- .../src/services/grid_view_manager.rs | 54 ++++++++--- .../group/group_generator/checkbox_group.rs | 2 +- .../group/group_generator/generator.rs | 2 +- .../group_generator/select_option_group.rs | 4 +- .../src/services/group/group_service.rs | 32 ++----- .../src/services/setting/setting_builder.rs | 6 +- .../tests/grid/block_test/script.rs | 3 +- .../tests/grid/filter_test/script.rs | 6 +- shared-lib/flowy-error-code/src/code.rs | 3 + .../src/client_grid/block_revision_pad.rs | 8 +- 22 files changed, 295 insertions(+), 176 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index 12e0f90b01..816f32fe16 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -18,14 +18,13 @@ class FieldService { FieldService({required this.gridId, required this.fieldId}); Future> moveField(int fromIndex, int toIndex) { - final payload = MoveItemPayloadPB.create() + final payload = MoveFieldPayloadPB.create() ..gridId = gridId - ..itemId = fieldId - ..ty = MoveItemTypePB.MoveField + ..fieldId = fieldId ..fromIndex = fromIndex ..toIndex = toIndex; - return GridEventMoveItem(payload).send(); + return GridEventMoveField(payload).send(); } Future> updateField({ diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index 0f056a4006..743f94ffbe 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -4,6 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart'; class RowFFIService { final String gridId; @@ -21,16 +22,25 @@ class RowFFIService { return GridEventCreateRow(payload).send(); } - Future> moveRow( - String rowId, int fromIndex, int toIndex) { - final payload = MoveItemPayloadPB.create() - ..gridId = gridId - ..itemId = rowId - ..ty = MoveItemTypePB.MoveRow + Future> moveRow({ + required String rowId, + required int fromIndex, + required int toIndex, + required GridLayout layout, + String? upperRowId, + }) { + var payload = MoveRowPayloadPB.create() + ..viewId = gridId + ..rowId = rowId + ..layout = layout ..fromIndex = fromIndex ..toIndex = toIndex; - return GridEventMoveItem(payload).send(); + if (upperRowId != null) { + payload.upperRowId = upperRowId; + } + + return GridEventMoveRow(payload).send(); } Future> getRow() { diff --git a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs index 6657e0e05a..f4200d81cf 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs @@ -1,5 +1,5 @@ -use crate::entities::{BlockPB, FieldIdPB}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use crate::entities::{BlockPB, FieldIdPB, GridLayout}; +use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -52,25 +52,51 @@ impl std::convert::From<&str> for GridBlockIdPB { } } -#[derive(Debug, Clone, ProtoBuf_Enum)] -pub enum MoveItemTypePB { - MoveField = 0, - MoveRow = 1, -} - -impl std::default::Default for MoveItemTypePB { - fn default() -> Self { - MoveItemTypePB::MoveField - } -} - #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MoveItemPayloadPB { +pub struct MoveFieldPayloadPB { #[pb(index = 1)] pub grid_id: String, #[pb(index = 2)] - pub item_id: String, + pub field_id: String, + + #[pb(index = 3)] + pub from_index: i32, + + #[pb(index = 4)] + pub to_index: i32, +} + +#[derive(Clone)] +pub struct MoveFieldParams { + pub grid_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 grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?; + Ok(MoveFieldParams { + grid_id: grid_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 row_id: String, #[pb(index = 3)] pub from_index: i32, @@ -79,30 +105,38 @@ pub struct MoveItemPayloadPB { pub to_index: i32, #[pb(index = 5)] - pub ty: MoveItemTypePB, + pub layout: GridLayout, + + #[pb(index = 6, one_of)] + pub upper_row_id: Option, } -#[derive(Clone)] -pub struct MoveItemParams { - pub grid_id: String, - pub item_id: String, +pub struct MoveRowParams { + pub view_id: String, + pub row_id: String, pub from_index: i32, pub to_index: i32, - pub ty: MoveItemTypePB, + pub layout: GridLayout, + pub upper_row_id: Option, } -impl TryInto for MoveItemPayloadPB { +impl TryInto for MoveRowPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { - let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; - let item_id = NotEmptyStr::parse(self.item_id).map_err(|_| ErrorCode::InvalidData)?; - Ok(MoveItemParams { - grid_id: grid_id.0, - item_id: item_id.0, + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::GridViewIdIsEmpty)?; + let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; + let upper_row_id = match self.upper_row_id { + None => None, + Some(upper_row_id) => Some(NotEmptyStr::parse(upper_row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?.0), + }; + Ok(MoveRowParams { + view_id: view_id.0, + row_id: row_id.0, from_index: self.from_index, to_index: self.to_index, - ty: self.ty, + layout: self.layout, + upper_row_id, }) } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs index 1ce2b40358..3a86a623ff 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs @@ -1,4 +1,4 @@ -use crate::entities::{CreateRowParams, RowPB}; +use crate::entities::{CreateRowParams, GridLayout, RowPB}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -22,45 +22,46 @@ impl TryInto for CreateBoardCardPayloadPB { grid_id: grid_id.0, start_row_id: None, group_id: Some(group_id.0), + layout: GridLayout::Board, }) } } #[derive(Debug, Default, ProtoBuf)] -pub struct BoardCardChangesetPB { +pub struct GroupRowsChangesetPB { #[pb(index = 1)] pub group_id: String, #[pb(index = 2)] - pub inserted_cards: Vec, + pub inserted_rows: Vec, #[pb(index = 3)] - pub deleted_cards: Vec, + pub deleted_rows: Vec, #[pb(index = 4)] - pub updated_cards: Vec, + pub updated_rows: Vec, } -impl BoardCardChangesetPB { - pub fn insert(group_id: String, inserted_cards: Vec) -> Self { +impl GroupRowsChangesetPB { + pub fn insert(group_id: String, inserted_rows: Vec) -> Self { Self { group_id, - inserted_cards, + inserted_rows, ..Default::default() } } - pub fn delete(group_id: String, deleted_cards: Vec) -> Self { + pub fn delete(group_id: String, deleted_rows: Vec) -> Self { Self { group_id, - deleted_cards, + deleted_rows, ..Default::default() } } - pub fn update(group_id: String, updated_cards: Vec) -> Self { + pub fn update(group_id: String, updated_rows: Vec) -> Self { Self { group_id, - updated_cards, + updated_rows, ..Default::default() } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs index d42052c747..398351371c 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs @@ -1,3 +1,4 @@ +use crate::entities::GridLayout; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -46,7 +47,7 @@ pub struct BlockRowIdPB { } #[derive(ProtoBuf, Default)] -pub struct CreateRowPayloadPB { +pub struct CreateTableRowPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -59,17 +60,20 @@ pub struct CreateRowParams { pub grid_id: String, pub start_row_id: Option, pub group_id: Option, + pub layout: GridLayout, } -impl TryInto for CreateRowPayloadPB { +impl TryInto for CreateTableRowPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + Ok(CreateRowParams { grid_id: grid_id.0, start_row_id: self.start_row_id, group_id: None, + layout: GridLayout::Table, }) } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index e970249661..89f66dd433 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -19,7 +19,7 @@ pub struct GridSettingPB { pub layouts: Vec, #[pb(index = 2)] - pub current_layout_type: Layout, + pub current_layout_type: GridLayout, #[pb(index = 3)] pub filter_configuration_by_field_id: HashMap, @@ -34,13 +34,13 @@ pub struct GridSettingPB { #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct GridLayoutPB { #[pb(index = 1)] - ty: Layout, + ty: GridLayout, } impl GridLayoutPB { pub fn all() -> Vec { let mut layouts = vec![]; - for layout_ty in Layout::iter() { + for layout_ty in GridLayout::iter() { layouts.push(GridLayoutPB { ty: layout_ty }) } @@ -50,31 +50,31 @@ impl GridLayoutPB { #[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)] #[repr(u8)] -pub enum Layout { +pub enum GridLayout { Table = 0, Board = 1, } -impl std::default::Default for Layout { +impl std::default::Default for GridLayout { fn default() -> Self { - Layout::Table + GridLayout::Table } } -impl std::convert::From for Layout { +impl std::convert::From for GridLayout { fn from(rev: LayoutRevision) -> Self { match rev { - LayoutRevision::Table => Layout::Table, - LayoutRevision::Board => Layout::Board, + LayoutRevision::Table => GridLayout::Table, + LayoutRevision::Board => GridLayout::Board, } } } -impl std::convert::From for LayoutRevision { - fn from(layout: Layout) -> Self { +impl std::convert::From for LayoutRevision { + fn from(layout: GridLayout) -> Self { match layout { - Layout::Table => LayoutRevision::Table, - Layout::Board => LayoutRevision::Board, + GridLayout::Table => LayoutRevision::Table, + GridLayout::Board => LayoutRevision::Board, } } } @@ -85,7 +85,7 @@ pub struct GridSettingChangesetPayloadPB { pub grid_id: String, #[pb(index = 2)] - pub layout_type: Layout, + pub layout_type: GridLayout, #[pb(index = 3, one_of)] pub insert_filter: Option, diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 3001ecb52d..df86f18fe3 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -203,13 +203,13 @@ pub(crate) async fn create_field_type_option_data_handler( } #[tracing::instrument(level = "trace", skip(data, manager), err)] -pub(crate) async fn move_item_handler( - data: Data, +pub(crate) async fn move_field_handler( + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: MoveItemParams = data.into_inner().try_into()?; + let params: MoveFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; - let _ = editor.move_item(params).await?; + let _ = editor.move_field(params).await?; Ok(()) } @@ -260,8 +260,19 @@ pub(crate) async fn duplicate_row_handler( } #[tracing::instrument(level = "debug", skip(data, manager), err)] -pub(crate) async fn create_row_handler( - data: Data, +pub(crate) async fn move_row_handler( + data: Data, + manager: AppData>, +) -> Result<(), FlowyError> { + let params: MoveRowParams = data.into_inner().try_into()?; + let editor = manager.get_grid_editor(¶ms.view_id)?; + let _ = editor.move_row(params).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub(crate) async fn create_table_row_handler( + data: Data, manager: AppData>, ) -> DataResult { let params: CreateRowParams = data.into_inner().try_into()?; diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 980087fee0..55ef3ff4db 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -20,14 +20,15 @@ pub fn create(grid_manager: Arc) -> Module { .event(GridEvent::DeleteField, delete_field_handler) .event(GridEvent::SwitchToField, switch_to_field_handler) .event(GridEvent::DuplicateField, duplicate_field_handler) - .event(GridEvent::MoveItem, move_item_handler) + .event(GridEvent::MoveField, move_field_handler) .event(GridEvent::GetFieldTypeOption, get_field_type_option_data_handler) .event(GridEvent::CreateFieldTypeOption, create_field_type_option_data_handler) // Row - .event(GridEvent::CreateRow, create_row_handler) + .event(GridEvent::CreateTableRow, create_table_row_handler) .event(GridEvent::GetRow, get_row_handler) .event(GridEvent::DeleteRow, delete_row_handler) .event(GridEvent::DuplicateRow, duplicate_row_handler) + .event(GridEvent::MoveRow, move_row_handler) // Cell .event(GridEvent::GetCell, get_cell_handler) .event(GridEvent::UpdateCell, update_cell_handler) @@ -130,8 +131,8 @@ pub enum GridEvent { /// [MoveItem] event is used to move an item. For the moment, Item has two types defined in /// [MoveItemTypePB]. - #[event(input = "MoveItemPayloadPB")] - MoveItem = 22, + #[event(input = "MoveFieldPayloadPB")] + MoveField = 22, /// [FieldTypeOptionIdPB] event is used to get the FieldTypeOption data for a specific field type. /// @@ -166,8 +167,8 @@ pub enum GridEvent { #[event(input = "SelectOptionChangesetPayloadPB")] UpdateSelectOption = 32, - #[event(input = "CreateRowPayloadPB", output = "RowPB")] - CreateRow = 50, + #[event(input = "CreateTableRowPayloadPB", output = "RowPB")] + CreateTableRow = 50, /// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables /// to return a nullable row data. @@ -180,6 +181,9 @@ pub enum GridEvent { #[event(input = "RowIdPB")] DuplicateRow = 53, + #[event(input = "MoveRowPayloadPB")] + MoveRow = 54, + #[event(input = "GridCellIdPB", output = "GridCellPB")] GetCell = 70, diff --git a/frontend/rust-lib/flowy-grid/src/services/block_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_editor.rs index a53866ee48..64fcc87072 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_editor.rs @@ -59,7 +59,7 @@ impl GridBlockRevisionEditor { if let Some(start_row_id) = prev_row_id.as_ref() { match block_pad.index_of_row(start_row_id) { None => {} - Some(index) => row_index = Some(index + 1), + Some(index) => row_index = Some(index as i32 + 1), } } @@ -100,6 +100,10 @@ impl GridBlockRevisionEditor { Ok(()) } + pub async fn index_of_row(&self, row_id: &str) -> Option { + self.pad.read().await.index_of_row(row_id) + } + pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult>> { let row_ids = vec![Cow::Borrowed(row_id)]; let row_rev = self.get_row_revs(Some(row_ids)).await?.pop(); diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index 457215f8be..e6ada307db 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -52,7 +52,7 @@ impl GridBlockManager { } } - async fn get_editor_from_row_id(&self, row_id: &str) -> FlowyResult> { + pub(crate) async fn get_editor_from_row_id(&self, row_id: &str) -> FlowyResult> { let block_id = self.persistence.get_block_id(row_id)?; Ok(self.get_block_editor(&block_id).await?) } @@ -155,7 +155,7 @@ impl GridBlockManager { Ok(changesets) } - + // This function will be moved to GridViewRevisionEditor pub(crate) async fn move_row(&self, row_rev: Arc, from: usize, to: usize) -> FlowyResult<()> { let editor = self.get_editor_from_row_id(&row_rev.id).await?; let _ = editor.move_row(&row_rev.id, from, to).await?; @@ -180,6 +180,14 @@ impl GridBlockManager { Ok(()) } + // This function will be moved to GridViewRevisionEditor. + pub async fn index_of_row(&self, row_id: &str) -> Option { + match self.get_editor_from_row_id(row_id).await { + Ok(editor) => editor.index_of_row(row_id).await, + Err(_) => None, + } + } + pub async fn update_cell(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()> where F: FnOnce(Arc) -> RowPB, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index d1aa57a2a0..c39bb9e929 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -342,7 +342,7 @@ impl GridRevisionEditor { pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let _ = self.block_manager.delete_row(row_id).await?; - self.view_manager.delete_row(row_id).await; + self.view_manager.did_delete_row(row_id).await; Ok(()) } @@ -484,21 +484,22 @@ impl GridRevisionEditor { Ok(snapshots) } - pub async fn move_item(&self, params: MoveItemParams) -> FlowyResult<()> { - match params.ty { - MoveItemTypePB::MoveField => { - self.move_field(¶ms.item_id, params.from_index, params.to_index) - .await - } - MoveItemTypePB::MoveRow => self.move_row(¶ms.item_id, params.from_index, params.to_index).await, - } + pub async fn move_row(&self, params: MoveRowParams) -> FlowyResult<()> { + self.view_manager.move_row(params).await } - pub async fn move_field(&self, field_id: &str, from: i32, to: i32) -> FlowyResult<()> { + pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> { + let MoveFieldParams { + grid_id: _, + field_id, + from_index, + to_index, + } = params; + let _ = self - .modify(|grid_pad| Ok(grid_pad.move_field(field_id, from as usize, to as usize)?)) + .modify(|grid_pad| Ok(grid_pad.move_field(&field_id, from_index as usize, to_index as usize)?)) .await?; - if let Some((index, field_rev)) = self.grid_pad.read().await.get_field_rev(field_id) { + if let Some((index, field_rev)) = self.grid_pad.read().await.get_field_rev(&field_id) { let delete_field_order = FieldIdPB::from(field_id); let insert_field = IndexFieldPB::from_field_rev(field_rev, index); let notified_changeset = FieldChangesetPB { @@ -513,10 +514,6 @@ impl GridRevisionEditor { Ok(()) } - pub async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> { - self.view_manager.move_row(row_id, from, to).await - } - pub async fn delta_bytes(&self) -> Bytes { self.grid_pad.read().await.delta_bytes() } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 937d7d725b..84bd635b71 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,13 +1,16 @@ use flowy_error::{FlowyError, FlowyResult}; -use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, GroupPB, RowPB}; +use crate::entities::{ + CreateRowParams, GridFilterConfiguration, GridLayout, GridSettingPB, GroupPB, GroupRowsChangesetPB, RowPB, +}; use crate::services::grid_editor_task::GridServiceTaskScheduler; -use crate::services::group::{default_group_configuration, GroupConfigurationDelegate, GroupService}; +use crate::services::group::{default_group_configuration, Group, GroupConfigurationDelegate, GroupService}; use flowy_grid_data_model::revision::{FieldRevision, GroupConfigurationRevision, RowRevision}; use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder}; use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; use flowy_sync::entities::revision::Revision; +use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::services::setting::make_grid_setting; use flowy_sync::entities::grid::GridSettingChangesetParams; use lib_infra::future::{wrap_future, AFFuture, FutureResult}; @@ -19,7 +22,7 @@ pub trait GridViewRevisionDelegate: Send + Sync + 'static { fn get_field_rev(&self, field_id: &str) -> AFFuture>>; } -pub trait GridViewRevisionDataSource: Send + Sync + 'static { +pub trait GridViewRevisionRowDataSource: Send + Sync + 'static { fn row_revs(&self) -> AFFuture>>; } @@ -30,8 +33,9 @@ pub struct GridViewRevisionEditor { pad: Arc>, rev_manager: Arc, delegate: Arc, - data_source: Arc, + data_source: Arc, group_service: Arc>, + groups: Arc>>, scheduler: Arc, } @@ -47,7 +51,7 @@ impl GridViewRevisionEditor { ) -> FlowyResult where Delegate: GridViewRevisionDelegate, - DataSource: GridViewRevisionDataSource, + DataSource: GridViewRevisionRowDataSource, { let cloud = Arc::new(GridViewRevisionCloudService { token: token.to_owned(), @@ -57,52 +61,85 @@ impl GridViewRevisionEditor { let rev_manager = Arc::new(rev_manager); let group_service = GroupService::new(Box::new(pad.clone())).await; let user_id = user_id.to_owned(); + let groups = Arc::new(RwLock::new(vec![])); Ok(Self { pad, user_id, view_id, rev_manager, scheduler, + groups, delegate: Arc::new(delegate), data_source: Arc::new(data_source), group_service: Arc::new(RwLock::new(group_service)), }) } - pub(crate) async fn create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { - match params.group_id.as_ref() { - None => {} - Some(group_id) => { - self.group_service - .read() - .await - .update_row(row_rev, group_id, |field_id| self.delegate.get_field_rev(&field_id)) - .await; + pub(crate) async fn update_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { + match params.layout { + GridLayout::Table => { + // Table can be grouped too } + GridLayout::Board => match params.group_id.as_ref() { + None => {} + Some(group_id) => { + self.group_service + .read() + .await + .update_row(row_rev, group_id, |field_id| self.delegate.get_field_rev(&field_id)) + .await; + } + }, } + todo!() } pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { + // Send the group notification if the current view has groups match params.group_id.as_ref() { None => {} Some(group_id) => { - self.group_service.read().await.did_create_row(group_id, row_pb).await; + let changeset = GroupRowsChangesetPB::insert(group_id.clone(), vec![row_pb.clone()]); + self.notify_did_update_group(changeset).await; } } } - pub(crate) async fn delete_row(&self, row_id: &str) { - self.group_service.read().await.did_delete_card(row_id.to_owned()).await; + pub(crate) async fn did_delete_row(&self, row_id: &str) { + // Send the group notification if the current view has groups; + match self.group_id_of_row(row_id).await { + None => {} + Some(group_id) => { + let changeset = GroupRowsChangesetPB::delete(group_id, vec![row_id.to_owned()]); + self.notify_did_update_group(changeset).await; + } + } + } + + async fn group_id_of_row(&self, row_id: &str) -> Option { + let read_guard = self.groups.read().await; + for group in read_guard.iter() { + if group.rows.iter().find(|row| row.id == row_id).is_some() { + return Some(group.id.clone()); + } + } + + None } pub(crate) async fn load_groups(&self) -> FlowyResult> { let field_revs = self.delegate.get_field_revs().await; let row_revs = self.data_source.row_revs().await; + + // let mut write_guard = self.group_service.write().await; match write_guard.load_groups(&field_revs, row_revs).await { None => Ok(vec![]), - Some(groups) => Ok(groups), + Some(groups) => { + *self.groups.write().await = groups.clone(); + Ok(groups.into_iter().map(GroupPB::from).collect()) + } } } @@ -129,6 +166,12 @@ impl GridViewRevisionEditor { } } + async fn notify_did_update_group(&self, changeset: GroupRowsChangesetPB) { + send_dart_notification(&changeset.group_id, GridNotification::DidUpdateBoard) + .payload(changeset) + .send(); + } + async fn modify(&self, f: F) -> FlowyResult<()> where F: for<'a> FnOnce(&'a mut GridViewRevisionPad) -> FlowyResult>, diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index cdaf72665e..edae70ba63 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -1,8 +1,12 @@ -use crate::entities::{CreateRowParams, GridFilterConfiguration, GridSettingPB, RepeatedGridGroupPB, RowPB}; +use crate::entities::{ + CreateRowParams, GridFilterConfiguration, GridLayout, GridSettingPB, MoveRowParams, RepeatedGridGroupPB, RowPB, +}; use crate::manager::GridUser; use crate::services::block_manager::GridBlockManager; use crate::services::grid_editor_task::GridServiceTaskScheduler; -use crate::services::grid_view_editor::{GridViewRevisionDataSource, GridViewRevisionDelegate, GridViewRevisionEditor}; +use crate::services::grid_view_editor::{ + GridViewRevisionDelegate, GridViewRevisionEditor, GridViewRevisionRowDataSource, +}; use bytes::Bytes; use dashmap::DashMap; use flowy_error::FlowyResult; @@ -45,7 +49,7 @@ impl GridViewManager { pub(crate) async fn update_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { for view_editor in self.view_editors.iter() { - view_editor.create_row(row_rev, params).await; + view_editor.update_row(row_rev, params).await; } } @@ -55,9 +59,9 @@ impl GridViewManager { } } - pub(crate) async fn delete_row(&self, row_id: &str) { + pub(crate) async fn did_delete_row(&self, row_id: &str) { for view_editor in self.view_editors.iter() { - view_editor.delete_row(row_id).await; + view_editor.did_delete_row(row_id).await; } } @@ -83,15 +87,35 @@ impl GridViewManager { Ok(RepeatedGridGroupPB { items: groups }) } - pub(crate) async fn move_row(&self, row_id: &str, from: i32, to: i32) -> FlowyResult<()> { - match self.block_manager.get_row_rev(row_id).await? { + pub(crate) async fn move_row(&self, params: MoveRowParams) -> FlowyResult<()> { + let MoveRowParams { + view_id: _, + row_id, + from_index, + to_index, + layout, + upper_row_id, + } = params; + + let from_index = from_index as usize; + + match self.block_manager.get_row_rev(&row_id).await? { None => tracing::warn!("Move row failed, can not find the row:{}", row_id), - Some(row_rev) => { - let _ = self - .block_manager - .move_row(row_rev.clone(), from as usize, to as usize) - .await?; - } + Some(row_rev) => match layout { + GridLayout::Table => { + tracing::trace!("Move row from {} to {}", from_index, to_index); + let to_index = to_index as usize; + let _ = self.block_manager.move_row(row_rev, from_index, to_index).await?; + } + GridLayout::Board => { + if let Some(upper_row_id) = upper_row_id { + if let Some(to_index) = self.block_manager.index_of_row(&upper_row_id).await { + tracing::trace!("Move row from {} to {}", from_index, to_index); + let _ = self.block_manager.move_row(row_rev, from_index, to_index).await?; + } + } + } + }, } Ok(()) } @@ -132,7 +156,7 @@ async fn make_view_editor( ) -> FlowyResult where Delegate: GridViewRevisionDelegate, - DataSource: GridViewRevisionDataSource, + DataSource: GridViewRevisionRowDataSource, { tracing::trace!("Open view:{} editor", view_id); @@ -170,7 +194,7 @@ impl RevisionCompactor for GridViewRevisionCompactor { } } -impl GridViewRevisionDataSource for Arc { +impl GridViewRevisionRowDataSource for Arc { fn row_revs(&self) -> AFFuture>> { let block_manager = self.clone(); diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs index 6ea0c78be2..01c2e7a2d9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/checkbox_group.rs @@ -22,7 +22,7 @@ impl GroupActionHandler for CheckboxGroupController { &self.field_id } - fn get_groups(&self) -> Vec { + fn build_groups(&self) -> Vec { self.make_groups() } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs index 0f15538acb..11144184a8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/generator.rs @@ -28,7 +28,7 @@ pub trait Groupable { pub trait GroupActionHandler: Send + Sync { fn field_id(&self) -> &str; - fn get_groups(&self) -> Vec; + fn build_groups(&self) -> Vec; fn group_rows(&mut self, row_revs: &[Arc], field_rev: &FieldRevision) -> FlowyResult<()>; fn update_card(&self, row_rev: &mut RowRevision, field_rev: &FieldRevision, group_id: &str); } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs index 933e3f936e..eb9ce69c9d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_generator/select_option_group.rs @@ -30,7 +30,7 @@ impl GroupActionHandler for SingleSelectGroupController { &self.field_id } - fn get_groups(&self) -> Vec { + fn build_groups(&self) -> Vec { self.make_groups() } @@ -94,7 +94,7 @@ impl GroupActionHandler for MultiSelectGroupController { &self.field_id } - fn get_groups(&self) -> Vec { + fn build_groups(&self) -> Vec { self.make_groups() } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 266abee6d7..77673470e3 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,11 +1,11 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ - BoardCardChangesetPB, CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, + CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, GroupRowsChangesetPB, NumberGroupConfigurationPB, RowPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, }; use crate::services::group::{ - CheckboxGroupController, GroupActionHandler, MultiSelectGroupController, SingleSelectGroupController, + CheckboxGroupController, Group, GroupActionHandler, MultiSelectGroupController, SingleSelectGroupController, }; use bytes::Bytes; use flowy_error::FlowyResult; @@ -36,7 +36,7 @@ impl GroupService { &mut self, field_revs: &[Arc], row_revs: Vec>, - ) -> Option> { + ) -> Option> { let field_rev = find_group_field(field_revs).unwrap(); let field_type: FieldType = field_rev.field_type_rev.into(); let configuration = self.delegate.get_group_configuration(field_rev.clone()).await; @@ -79,26 +79,6 @@ impl GroupService { // let row_pb = make_row_from_row_rev(row_rev); todo!() } - #[allow(dead_code)] - pub async fn did_delete_card(&self, _row_id: String) { - // let changeset = BoardCardChangesetPB::delete(group_id.to_owned(), vec![row_id]); - // self.notify_did_update_board(changeset).await; - todo!() - } - - pub async fn did_create_row(&self, group_id: &str, row_pb: &RowPB) { - let changeset = BoardCardChangesetPB::insert(group_id.to_owned(), vec![row_pb.clone()]); - self.notify_did_update_board(changeset).await; - } - - pub async fn notify_did_update_board(&self, changeset: BoardCardChangesetPB) { - if self.action_handler.is_none() { - return; - } - send_dart_notification(&changeset.group_id, GridNotification::DidUpdateBoard) - .payload(changeset) - .send(); - } #[tracing::instrument(level = "trace", skip_all, err)] async fn build_groups( @@ -107,7 +87,7 @@ impl GroupService { field_rev: &Arc, row_revs: Vec>, configuration: GroupConfigurationRevision, - ) -> FlowyResult> { + ) -> FlowyResult> { match field_type { FieldType::RichText => { // let generator = GroupGenerator::::from_configuration(configuration); @@ -139,11 +119,11 @@ impl GroupService { if let Some(group_action_handler) = self.action_handler.as_ref() { let mut write_guard = group_action_handler.write().await; let _ = write_guard.group_rows(&row_revs, field_rev)?; - groups = write_guard.get_groups(); + groups = write_guard.build_groups(); drop(write_guard); } - Ok(groups.into_iter().map(GroupPB::from).collect()) + Ok(groups) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index 6eb481c74f..7d285fbe2b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -1,5 +1,5 @@ use crate::entities::{ - GridLayoutPB, GridSettingPB, Layout, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, + GridLayout, GridLayoutPB, GridSettingPB, RepeatedGridConfigurationFilterPB, RepeatedGridGroupConfigurationPB, RepeatedGridSortPB, }; use flowy_grid_data_model::revision::{FieldRevision, SettingRevision}; @@ -12,7 +12,7 @@ pub struct GridSettingChangesetBuilder { } impl GridSettingChangesetBuilder { - pub fn new(grid_id: &str, layout_type: &Layout) -> Self { + pub fn new(grid_id: &str, layout_type: &GridLayout) -> Self { let params = GridSettingChangesetParams { grid_id: grid_id.to_string(), layout_type: layout_type.clone().into(), @@ -42,7 +42,7 @@ impl GridSettingChangesetBuilder { } pub fn make_grid_setting(grid_setting_rev: &SettingRevision, field_revs: &[Arc]) -> GridSettingPB { - let current_layout_type: Layout = grid_setting_rev.layout.clone().into(); + let current_layout_type: GridLayout = grid_setting_rev.layout.clone().into(); let filters_by_field_id = grid_setting_rev .get_all_filters(field_revs) .map(|filters_by_field_id| { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index 2c54f8561e..54a022e0d6 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -2,7 +2,7 @@ use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow}; use crate::grid::block_test::util::GridRowTestBuilder; use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::{CreateRowParams, FieldType, GridCellIdParams, RowPB}; +use flowy_grid::entities::{CreateRowParams, FieldType, GridCellIdParams, GridLayout, RowPB}; use flowy_grid::services::field::*; use flowy_grid_data_model::revision::{ GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, @@ -81,6 +81,7 @@ impl GridRowTest { grid_id: self.editor.grid_id.clone(), start_row_id: None, group_id: None, + layout: GridLayout::Table, }; let row_order = self.editor.create_row(params).await.unwrap(); self.row_order_by_row_id diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index 03ba9fa29c..3320204ae3 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] #![allow(unused_imports)] -use flowy_grid::entities::{CreateGridFilterPayloadPB, Layout, GridSettingPB}; +use flowy_grid::entities::{CreateGridFilterPayloadPB, GridLayout, GridSettingPB}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; @@ -55,7 +55,7 @@ impl GridFilterTest { } FilterScript::InsertGridTableFilter { payload } => { let params: CreateGridFilterParams = payload.try_into().unwrap(); - let layout_type = Layout::Table; + let layout_type = GridLayout::Table; let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) .insert_filter(params) .build(); @@ -66,7 +66,7 @@ impl GridFilterTest { assert_eq!(count as usize, filters.len()); } FilterScript::DeleteGridTableFilter { filter_id, field_rev} => { - let layout_type = Layout::Table; + let layout_type = GridLayout::Table; let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) .delete_filter(DeleteFilterParams { field_id: field_rev.id, filter_id, field_type_rev: field_rev.field_type_rev }) .build(); diff --git a/shared-lib/flowy-error-code/src/code.rs b/shared-lib/flowy-error-code/src/code.rs index 5b676073da..63f7ac7749 100644 --- a/shared-lib/flowy-error-code/src/code.rs +++ b/shared-lib/flowy-error-code/src/code.rs @@ -91,6 +91,9 @@ pub enum ErrorCode { #[display(fmt = "Grid id is empty")] GridIdIsEmpty = 410, + #[display(fmt = "Grid view id is empty")] + GridViewIdIsEmpty = 411, + #[display(fmt = "Grid block id is empty")] BlockIdIsEmpty = 420, #[display(fmt = "Row id is empty")] diff --git a/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs index 4215da7572..142463b8fc 100644 --- a/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs @@ -139,12 +139,8 @@ impl GridBlockRevisionPad { self.block.rows.len() as i32 } - pub fn index_of_row(&self, row_id: &str) -> Option { - self.block - .rows - .iter() - .position(|row| row.id == row_id) - .map(|index| index as i32) + pub fn index_of_row(&self, row_id: &str) -> Option { + self.block.rows.iter().position(|row| row.id == row_id) } pub fn update_row(&mut self, changeset: RowMetaChangeset) -> CollaborateResult> { From 1fc443d9acc7687b123c457eebcd817ce00c5f7f Mon Sep 17 00:00:00 2001 From: Vincent Chan Date: Mon, 15 Aug 2022 17:22:55 +0800 Subject: [PATCH 160/224] feat: place holder --- .../example/lib/plugin/image_node_widget.dart | 71 +++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 7a47802163..4270897397 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -34,6 +34,8 @@ class ImageNodeBuilder extends NodeWidgetBuilder { }); } +const double placeholderHeight = 132; + class ImageNodeWidget extends StatefulWidget { final Node node; final EditorState editorState; @@ -49,6 +51,7 @@ class ImageNodeWidget extends StatefulWidget { } class _ImageNodeWidgetState extends State with Selectable { + bool isHovered = false; Node get node => widget.node; EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; @@ -88,13 +91,73 @@ class _ImageNodeWidgetState extends State with Selectable { return _build(context); } + Widget _loadingBuilder( + BuildContext context, Widget widget, ImageChunkEvent? evt) { + if (evt == null) { + return widget; + } + return Container( + alignment: Alignment.center, + height: placeholderHeight, + child: const Text("Loading..."), + ); + } + + Widget _errorBuilder( + BuildContext context, Object obj, StackTrace? stackTrace) { + return Container( + alignment: Alignment.center, + height: placeholderHeight, + child: const Text("Error..."), + ); + } + + Widget _frameBuilder( + BuildContext context, + Widget child, + int? frame, + bool wasSynchronouslyLoaded, + ) { + if (frame == null) { + return Container( + alignment: Alignment.center, + height: placeholderHeight, + child: const Text("Loading..."), + ); + } + + return child; + } + Widget _build(BuildContext context) { return Column( children: [ - Image.network( - src, - width: MediaQuery.of(context).size.width, - ) + MouseRegion( + onEnter: (event) { + setState(() { + isHovered = true; + }); + }, + onExit: (event) { + setState(() { + isHovered = false; + }); + }, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + border: Border.all( + color: isHovered ? Colors.blue : Colors.grey, + ), + borderRadius: const BorderRadius.all(Radius.circular(20))), + child: Image.network( + src, + width: MediaQuery.of(context).size.width, + frameBuilder: _frameBuilder, + loadingBuilder: _loadingBuilder, + errorBuilder: _errorBuilder, + ), + )), ], ); } From 2d04f79e101fd9da9212f24f575b3ac0fab4d006 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 16:11:46 +0800 Subject: [PATCH 161/224] chore: publish preparation --- .../flowy_editor/example/assets/example.json | 79 ++++--------------- .../src/render/rich_text/rich_text_style.dart | 4 +- .../src/render/selection/toolbar_widget.dart | 9 +-- .../packages/flowy_editor/pubspec.yaml | 1 + 4 files changed, 23 insertions(+), 70 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index e6357d93f9..549cd4f765 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -6,7 +6,7 @@ { "type": "image", "attributes": { - "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" + "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=500w" } }, { @@ -25,7 +25,7 @@ "type": "text", "delta": [ { - "insert": "👋 Welcome to Appflowy" + "insert": "👋 Welcome to FlowyEditor" } ], "attributes": { @@ -37,11 +37,16 @@ "type": "text", "delta": [ { - "insert": "At " + "insert": "We are still developing more features. Please give us a star if the " }, - { "insert": "AppFlowy", "attributes": { "code": true, "bold": true, "color": "0xFFED459C"} }, { - "insert": ", we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." + "insert": "FlowyEditor", + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + } + }, + { + "insert": " helps you." } ] }, @@ -76,7 +81,7 @@ "attributes": { "backgroundColor": "0xFFFFFF00" } }, { - "insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc." + "insert": "to see all the types of content you can add - headers, bulleted lists, checkboxes, etc." } ] }, @@ -84,7 +89,11 @@ "type": "text", "delta": [ { - "insert": "Highlight any text, and use the menu that pops up to " + "insert": "Highlight", + "attributes": { "backgroundColor": "0xFF00BCFB" } + }, + { + "insert": " any text, and use the menu that pops up to " }, { "insert": "style", "attributes": { "bold": true } }, { "insert": " your ", "attributes": { "italic": true } }, @@ -245,62 +254,6 @@ "subtype": "number-list", "number": 3 } - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] - }, - { - "type": "text", - "delta": [ - { - "insert": "At AppFlowy, we embody what we value deep in our hearts, taking inspiration from other great companies while forging our own path. AppFlowy’s five core values are Mission Driven, Aim High & Iterate, Transparency, Collaboration, and Honesty. Together, they spell MATCH. We will continue to iterate and refine these values as we grow." - } - ] } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart index 31cbafc18b..178f5de5b8 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -2,6 +2,7 @@ import 'package:flowy_editor/src/document/attributes.dart'; import 'package:flowy_editor/src/document/node.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher_string.dart'; /// /// Supported partial rendering types: @@ -235,7 +236,6 @@ class RichTextStyle { var decorations = [TextDecoration.none]; if (attributes.underline || attributes.href != null) { decorations.add(TextDecoration.underline); - // TextDecoration.underline; } if (attributes.strikethrough) { decorations.add(TextDecoration.lineThrough); @@ -275,7 +275,7 @@ class RichTextStyle { if (href != null) { return TapGestureRecognizer() ..onTap = () async { - // FIXME: launch the url + await launchUrlString(href); }; } return null; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart index 2c9fb9ad93..5d6ae6bedf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/toolbar_widget.dart @@ -15,7 +15,6 @@ ToolbarEventHandlers defaultToolbarEventHandlers = { 'strikethrough': (editorState) => formatStrikethrough(editorState), 'underline': (editorState) => formatUnderline(editorState), 'quote': (editorState) => formatQuote(editorState), - 'number_list': (editorState) {}, 'bulleted_list': (editorState) => formatBulletedList(editorState), 'Text': (editorState) => formatText(editorState), 'H1': (editorState) => formatHeading(editorState, StyleKey.h1), @@ -94,15 +93,15 @@ class _ToolbarWidgetState extends State with ToolbarMixin { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _listToolbar(context), - _centerToolbarIcon('divider', width: 10), + // _listToolbar(context), + // _centerToolbarIcon('divider', width: 10), _centerToolbarIcon('bold'), _centerToolbarIcon('italic'), _centerToolbarIcon('strikethrough'), _centerToolbarIcon('underline'), - _centerToolbarIcon('divider', width: 10), + _centerToolbarIcon('divider', width: 2), _centerToolbarIcon('quote'), - _centerToolbarIcon('number_list'), + // _centerToolbarIcon('number_list'), _centerToolbarIcon('bulleted_list'), ], ), diff --git a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml index 1f84c47ef6..948405701c 100644 --- a/frontend/app_flowy/packages/flowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/flowy_editor/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: html: ^0.15.0 flutter_svg: ^1.1.1+1 provider: ^6.0.3 + url_launcher: ^6.1.5 dev_dependencies: flutter_test: From bc346dfd67957073d9e1c39065cd5ff7c5c87ebc Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 17:13:56 +0800 Subject: [PATCH 162/224] chore: create board card --- .../plugins/board/application/board_bloc.dart | 57 +++++++++++++++---- .../application/board_data_controller.dart | 10 ++-- .../board/application/group_controller.dart | 49 ++++++++++++++++ .../board/application/group_listener.dart | 51 +++++++++++++++++ .../board/presentation/board_page.dart | 2 +- .../grid/application/grid_service.dart | 4 +- .../grid/application/row/row_service.dart | 4 +- .../appflowy_board/lib/src/widgets/board.dart | 4 +- .../widgets/board_column/board_column.dart | 4 +- .../lib/src/widgets/board_data.dart | 2 +- .../src/widgets/reorder_flex/drag_target.dart | 10 ++-- ...ptor.dart => drag_target_interceptor.dart} | 6 +- .../widgets/reorder_flex/reorder_flex.dart | 16 +++--- .../reorder_phantom/phantom_controller.dart | 9 +-- .../flowy-grid/src/dart_notification.rs | 2 +- .../src/entities/group_entities/board_card.rs | 42 +------------- .../group_entities/group_changeset.rs | 43 ++++++++++++++ .../src/entities/group_entities/mod.rs | 2 + .../src/services/grid_view_editor.rs | 15 +++-- .../src/services/group/group_service.rs | 6 +- 20 files changed, 240 insertions(+), 98 deletions(-) create mode 100644 frontend/app_flowy/lib/plugins/board/application/group_controller.dart create mode 100644 frontend/app_flowy/lib/plugins/board/application/group_listener.dart rename frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/{drag_target_inteceptor.dart => drag_target_interceptor.dart} (96%) create mode 100644 frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index a4a892a7fc..8afdb15051 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -14,12 +14,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:collection'; import 'board_data_controller.dart'; +import 'group_controller.dart'; part 'board_bloc.freezed.dart'; class BoardBloc extends Bloc { final BoardDataController _dataController; - late final AFBoardDataController boardDataController; + late final AFBoardDataController afBoardDataController; + List groupControllers = []; GridFieldCache get fieldCache => _dataController.fieldCache; String get gridId => _dataController.gridId; @@ -27,7 +29,7 @@ class BoardBloc extends Bloc { BoardBloc({required ViewPB view}) : _dataController = BoardDataController(view: view), super(BoardState.initial(view.id)) { - boardDataController = AFBoardDataController( + afBoardDataController = AFBoardDataController( onMoveColumn: ( fromIndex, toIndex, @@ -71,9 +73,6 @@ class BoardBloc extends Bloc { didReceiveGridUpdate: (GridPB grid) { emit(state.copyWith(grid: Some(grid))); }, - didReceiveGroups: (List groups) { - emit(state.copyWith(groups: groups)); - }, didReceiveRows: (List rowInfos) { emit(state.copyWith(rowInfos: rowInfos)); }, @@ -85,9 +84,24 @@ class BoardBloc extends Bloc { @override Future close() async { await _dataController.dispose(); + for (final controller in groupControllers) { + controller.dispose(); + } return super.close(); } + void initializeGroups(List groups) { + for (final group in groups) { + final delegate = GroupControllerDelegateImpl(afBoardDataController); + final controller = GroupController( + group: group, + delegate: delegate, + ); + controller.startListening(); + groupControllers.add(controller); + } + } + GridRowCache? getRowCache(String blockId) { final GridBlockCache? blockCache = _dataController.blocks[blockId]; return blockCache?.rowCache; @@ -100,7 +114,7 @@ class BoardBloc extends Bloc { add(BoardEvent.didReceiveGridUpdate(grid)); } }, - onGroupChanged: (groups) { + didLoadGroups: (groups) { List columns = groups.map((group) { return AFBoardColumnData( id: group.groupId, @@ -110,7 +124,8 @@ class BoardBloc extends Bloc { ); }).toList(); - boardDataController.addColumns(columns); + afBoardDataController.addColumns(columns); + initializeGroups(groups); }, onRowsChanged: (List rowInfos, RowsChangedReason reason) { add(BoardEvent.didReceiveRows(rowInfos)); @@ -155,8 +170,6 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = InitialGrid; const factory BoardEvent.createRow(String groupId) = _CreateRow; const factory BoardEvent.endEditRow(String rowId) = _EndEditRow; - const factory BoardEvent.didReceiveGroups(List groups) = - _DidReceiveGroup; const factory BoardEvent.didReceiveRows(List rowInfos) = _DidReceiveRows; const factory BoardEvent.didReceiveGridUpdate( @@ -169,7 +182,6 @@ class BoardState with _$BoardState { const factory BoardState({ required String gridId, required Option grid, - required List groups, required Option editingRow, required List rowInfos, required GridLoadingState loadingState, @@ -177,7 +189,6 @@ class BoardState with _$BoardState { factory BoardState.initial(String gridId) => BoardState( rowInfos: [], - groups: [], grid: none(), gridId: gridId, editingRow: none(), @@ -228,3 +239,27 @@ class CreateCardItem extends AFColumnItem { @override String get id => '$CreateCardItem'; } + +class GroupControllerDelegateImpl extends GroupControllerDelegate { + final AFBoardDataController controller; + + GroupControllerDelegateImpl(this.controller); + + @override + void insertRow(String groupId, RowPB row, int? index) { + final item = BoardColumnItem(row: row); + if (index != null) { + controller.insertColumnItem(groupId, index, item); + } else { + controller.addColumnItem(groupId, item); + } + } + + @override + void removeRow(String groupId, String rowId) { + controller.removeColumnItem(groupId, rowId); + } + + @override + void updateRow(String groupId, RowPB row) {} +} diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index e4b4f90520..1d17431713 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -12,7 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); -typedef OnGroupChanged = void Function(List); +typedef DidLoadGroups = void Function(List); typedef OnRowsChanged = void Function( List, RowsChangedReason, @@ -30,7 +30,7 @@ class BoardDataController { OnFieldsChanged? _onFieldsChanged; OnGridChanged? _onGridChanged; - OnGroupChanged? _onGroupChanged; + DidLoadGroups? _didLoadGroup; OnRowsChanged? _onRowsChanged; OnError? _onError; @@ -51,13 +51,13 @@ class BoardDataController { void addListener({ OnGridChanged? onGridChanged, OnFieldsChanged? onFieldsChanged, - OnGroupChanged? onGroupChanged, + DidLoadGroups? didLoadGroups, OnRowsChanged? onRowsChanged, OnError? onError, }) { _onGridChanged = onGridChanged; _onFieldsChanged = onFieldsChanged; - _onGroupChanged = onGroupChanged; + _didLoadGroup = didLoadGroups; _onRowsChanged = onRowsChanged; _onError = onError; @@ -133,7 +133,7 @@ class BoardDataController { return Future( () => result.fold( (groups) { - _onGroupChanged?.call(groups.items); + _didLoadGroup?.call(groups.items); }, (err) => _onError?.call(err), ), diff --git a/frontend/app_flowy/lib/plugins/board/application/group_controller.dart b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart new file mode 100644 index 0000000000..a0cd5c3ced --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/group_controller.dart @@ -0,0 +1,49 @@ +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; + +import 'group_listener.dart'; + +abstract class GroupControllerDelegate { + void removeRow(String groupId, String rowId); + void insertRow(String groupId, RowPB row, int? index); + void updateRow(String groupId, RowPB row); +} + +class GroupController { + final GroupPB group; + final GroupListener _listener; + final GroupControllerDelegate delegate; + + GroupController({required this.group, required this.delegate}) + : _listener = GroupListener(group); + + void startListening() { + _listener.start(onGroupChanged: (result) { + result.fold( + (GroupRowsChangesetPB changeset) { + for (final insertedRow in changeset.insertedRows) { + final index = insertedRow.hasIndex() ? insertedRow.index : null; + delegate.insertRow( + group.groupId, + insertedRow.row, + index, + ); + } + + for (final deletedRow in changeset.deletedRows) { + delegate.removeRow(group.groupId, deletedRow); + } + + for (final updatedRow in changeset.updatedRows) { + delegate.updateRow(group.groupId, updatedRow); + } + }, + (err) => Log.error(err), + ); + }); + } + + Future dispose() async { + _listener.stop(); + } +} diff --git a/frontend/app_flowy/lib/plugins/board/application/group_listener.dart b/frontend/app_flowy/lib/plugins/board/application/group_listener.dart new file mode 100644 index 0000000000..797177deca --- /dev/null +++ b/frontend/app_flowy/lib/plugins/board/application/group_listener.dart @@ -0,0 +1,51 @@ +import 'dart:typed_data'; + +import 'package:app_flowy/core/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart'; + +typedef UpdateGroupNotifiedValue = Either; + +class GroupListener { + final GroupPB group; + PublishNotifier? _groupNotifier = PublishNotifier(); + GridNotificationListener? _listener; + GroupListener(this.group); + + void start({ + required void Function(UpdateGroupNotifiedValue) onGroupChanged, + }) { + _groupNotifier?.addPublishListener(onGroupChanged); + _listener = GridNotificationListener( + objectId: group.groupId, + handler: _handler, + ); + } + + void _handler( + GridNotification ty, + Either result, + ) { + switch (ty) { + case GridNotification.DidUpdateGroup: + result.fold( + (payload) => _groupNotifier?.value = + left(GroupRowsChangesetPB.fromBuffer(payload)), + (error) => _groupNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _groupNotifier?.dispose(); + _groupNotifier = null; + } +} diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index faf7e193b0..1fb31abcc7 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -55,7 +55,7 @@ class BoardContent extends StatelessWidget { child: AFBoard( // key: UniqueKey(), scrollController: ScrollController(), - dataController: context.read().boardDataController, + dataController: context.read().afBoardDataController, headerBuilder: _buildHeader, footBuilder: _buildFooter, cardBuilder: (_, data) => _buildCard(context, data), diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index 4315fff38b..8c46ce18b2 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -23,9 +23,9 @@ class GridFFIService { } Future> createRow({Option? startRowId}) { - CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId; + var payload = CreateTableRowPayloadPB.create()..gridId = gridId; startRowId?.fold(() => null, (id) => payload.startRowId = id); - return GridEventCreateRow(payload).send(); + return GridEventCreateTableRow(payload).send(); } Future> createBoardCard(String groupId) { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index 743f94ffbe..e610734064 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -15,11 +15,11 @@ class RowFFIService { {required this.gridId, required this.blockId, required this.rowId}); Future> createRow() { - CreateRowPayloadPB payload = CreateRowPayloadPB.create() + final payload = CreateTableRowPayloadPB.create() ..gridId = gridId ..startRowId = rowId; - return GridEventCreateRow(payload).send(); + return GridEventCreateTableRow(payload).send(); } Future> moveRow({ diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart index f4ce09fc2c..20824ba6b9 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'board_column/board_column.dart'; import 'board_column/board_column_data.dart'; import 'board_data.dart'; -import 'reorder_flex/drag_target_inteceptor.dart'; +import 'reorder_flex/drag_target_interceptor.dart'; import 'reorder_flex/reorder_flex.dart'; import 'reorder_phantom/phantom_controller.dart'; import '../rendering/board_overlay.dart'; @@ -143,7 +143,7 @@ class _BoardContentState extends State { void initState() { _overlayEntry = BoardOverlayEntry( builder: (BuildContext context) { - final interceptor = OverlappingDragTargetInteceptor( + final interceptor = OverlappingDragTargetInterceptor( reorderFlexId: widget.dataController.identifier, acceptedReorderFlexId: widget.dataController.columnIds, delegate: widget.delegate, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart index 3873bc222d..cbc537810e 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column.dart @@ -5,7 +5,7 @@ import '../../rendering/board_overlay.dart'; import '../../utils/log.dart'; import '../reorder_phantom/phantom_controller.dart'; import '../reorder_flex/reorder_flex.dart'; -import '../reorder_flex/drag_target_inteceptor.dart'; +import '../reorder_flex/drag_target_interceptor.dart'; import 'board_column_data.dart'; typedef OnColumnDragStarted = void Function(int index); @@ -37,7 +37,7 @@ typedef AFBoardColumnFooterBuilder = Widget Function( AFBoardColumnData columnData, ); -abstract class AFBoardColumnDataDataSource extends ReoderFlextDataSource { +abstract class AFBoardColumnDataDataSource extends ReoderFlexDataSource { AFBoardColumnData get columnData; List get acceptedColumnIds; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart index 6e3f45fc7d..6208dbd0f0 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_data.dart @@ -24,7 +24,7 @@ typedef OnMoveColumnItemToColumn = void Function( ); class AFBoardDataController extends ChangeNotifier - with EquatableMixin, BoardPhantomControllerDelegate, ReoderFlextDataSource { + with EquatableMixin, BoardPhantomControllerDelegate, ReoderFlexDataSource { final List _columnDatas = []; final OnMoveColumn? onMoveColumn; final OnMoveColumnItem? onMoveColumnItem; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart index a6a09a9770..132d3d9bc4 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target.dart @@ -13,14 +13,14 @@ abstract class ReorderFlexDraggableTargetBuilder { Widget child, DragTargetOnStarted onDragStarted, DragTargetOnEnded onDragEnded, - DragTargetWillAccpet onWillAccept, + DragTargetWillAccepted onWillAccept, AnimationController insertAnimationController, AnimationController deleteAnimationController, ); } /// -typedef DragTargetWillAccpet = bool Function( +typedef DragTargetWillAccepted = bool Function( T dragTargetData); /// @@ -51,7 +51,7 @@ class ReorderDragTarget extends StatefulWidget { /// /// [toAccept] represents the dragTarget index, which is the value passed in /// when creating the [ReorderDragTarget]. - final DragTargetWillAccpet onWillAccept; + final DragTargetWillAccepted onWillAccept; /// Called when an acceptable piece of data was dropped over this drag target. /// @@ -228,7 +228,7 @@ class DragTargetAnimation { value: 0.0, vsync: vsync, duration: const Duration(milliseconds: 10)); } - void startDargging() { + void startDragging() { entranceController.value = 1.0; } @@ -386,7 +386,7 @@ class FakeDragTarget extends StatefulWidget { final FakeDragTargetEventData eventData; final DragTargetOnStarted onDragStarted; final DragTargetOnEnded onDragEnded; - final DragTargetWillAccpet onWillAccept; + final DragTargetWillAccepted onWillAccept; final Widget child; final AnimationController insertAnimationController; final AnimationController deleteAnimationController; diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart similarity index 96% rename from frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart rename to frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart index da529819dd..be74b4eef8 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_inteceptor.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/drag_target_interceptor.dart @@ -40,18 +40,18 @@ abstract class OverlapDragTargetDelegate { bool canMoveTo(String dragTargetId); } -/// [OverlappingDragTargetInteceptor] is used to receive the overlapping +/// [OverlappingDragTargetInterceptor] is used to receive the overlapping /// [DragTarget] event. If a [DragTarget] child is [DragTarget], it will /// receive the [DragTarget] event when being dragged. /// /// Receive the [DragTarget] event if the [acceptedReorderFlexId] contains /// the passed in dragTarget' reorderFlexId. -class OverlappingDragTargetInteceptor extends DragTargetInterceptor { +class OverlappingDragTargetInterceptor extends DragTargetInterceptor { final String reorderFlexId; final List acceptedReorderFlexId; final OverlapDragTargetDelegate delegate; - OverlappingDragTargetInteceptor({ + OverlappingDragTargetInterceptor({ required this.delegate, required this.reorderFlexId, required this.acceptedReorderFlexId, diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart index 27af28d778..7fa1a405e1 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_flex/reorder_flex.dart @@ -7,25 +7,25 @@ import '../../utils/log.dart'; import 'reorder_mixin.dart'; import 'drag_target.dart'; import 'drag_state.dart'; -import 'drag_target_inteceptor.dart'; +import 'drag_target_interceptor.dart'; typedef OnDragStarted = void Function(int index); typedef OnDragEnded = void Function(); typedef OnReorder = void Function(int fromIndex, int toIndex); typedef OnDeleted = void Function(int deletedIndex); typedef OnInserted = void Function(int insertedIndex); -typedef OnReveivePassedInPhantom = void Function( +typedef OnReceivePassedInPhantom = void Function( FlexDragTargetData dragTargetData, int phantomIndex); -abstract class ReoderFlextDataSource { +abstract class ReoderFlexDataSource { /// [identifier] represents the id the [ReorderFlex]. It must be unique. String get identifier; - /// The number of [ReoderFlexItem]s will be displaied in the [ReorderFlex]. + /// The number of [ReoderFlexItem]s will be displayed in the [ReorderFlex]. UnmodifiableListView get items; } -/// Each item displaied in the [ReorderFlex] required to implement the [ReoderFlexItem]. +/// Each item displayed in the [ReorderFlex] required to implement the [ReoderFlexItem]. abstract class ReoderFlexItem { /// [id] is used to identify the item. It must be unique. String get id; @@ -70,7 +70,7 @@ class ReorderFlex extends StatefulWidget { /// [onDragEnded] is called when dragTarget did end dragging final OnDragEnded? onDragEnded; - final ReoderFlextDataSource dataSource; + final ReoderFlexDataSource dataSource; final DragTargetInterceptor? interceptor; @@ -187,7 +187,7 @@ class ReorderFlexState extends State void _requestAnimationToNextIndex({bool isAcceptingNewTarget = false}) { /// Update the dragState and animate to the next index if the current /// dragging animation is completed. Otherwise, it will get called again - /// when the animation finishs. + /// when the animation finish. if (_animation.entranceController.isCompleted) { dragState.removePhantom(); @@ -425,7 +425,7 @@ class ReorderFlexState extends State ) { setState(() { dragState.startDragging(draggingWidget, dragIndex, feedbackSize); - _animation.startDargging(); + _animation.startDragging(); }); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart index 1ab7b2da23..0db70d0bae 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/reorder_phantom/phantom_controller.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + import '../../utils/log.dart'; import '../board_column/board_column_data.dart'; import '../reorder_flex/drag_state.dart'; import '../reorder_flex/drag_target.dart'; -import '../reorder_flex/drag_target_inteceptor.dart'; +import '../reorder_flex/drag_target_interceptor.dart'; import 'phantom_state.dart'; abstract class BoardPhantomControllerDelegate { @@ -61,7 +62,7 @@ class BoardPhantomController extends OverlapDragTargetDelegate columnsState.setColumnIsDragging(columnId, false); } - /// Remove the phanton in the column when the column is end dragging. + /// Remove the phantom in the column when the column is end dragging. void columnEndDragging(String columnId) { columnsState.setColumnIsDragging(columnId, true); if (phantomRecord == null) return; @@ -331,7 +332,7 @@ class PhantomDraggableBuilder extends ReorderFlexDraggableTargetBuilder { Widget child, DragTargetOnStarted onDragStarted, DragTargetOnEnded onDragEnded, - DragTargetWillAccpet onWillAccept, + DragTargetWillAccepted onWillAccept, AnimationController insertAnimationController, AnimationController deleteAnimationController, ) { diff --git a/frontend/rust-lib/flowy-grid/src/dart_notification.rs b/frontend/rust-lib/flowy-grid/src/dart_notification.rs index 4108da1f11..0bba5bbc11 100644 --- a/frontend/rust-lib/flowy-grid/src/dart_notification.rs +++ b/frontend/rust-lib/flowy-grid/src/dart_notification.rs @@ -11,7 +11,7 @@ pub enum GridNotification { DidUpdateRow = 30, DidUpdateCell = 40, DidUpdateField = 50, - DidUpdateBoard = 60, + DidUpdateGroup = 60, } impl std::default::Default for GridNotification { diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs index 3a86a623ff..1ba3991f96 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/board_card.rs @@ -1,4 +1,4 @@ -use crate::entities::{CreateRowParams, GridLayout, RowPB}; +use crate::entities::{CreateRowParams, GridLayout}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -26,43 +26,3 @@ impl TryInto for CreateBoardCardPayloadPB { }) } } - -#[derive(Debug, Default, ProtoBuf)] -pub struct GroupRowsChangesetPB { - #[pb(index = 1)] - pub group_id: String, - - #[pb(index = 2)] - pub inserted_rows: Vec, - - #[pb(index = 3)] - pub deleted_rows: Vec, - - #[pb(index = 4)] - pub updated_rows: Vec, -} -impl GroupRowsChangesetPB { - 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() - } - } -} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs new file mode 100644 index 0000000000..1feb0debe2 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group_changeset.rs @@ -0,0 +1,43 @@ +use crate::entities::{InsertedRowPB, RowPB}; +use flowy_derive::ProtoBuf; + +#[derive(Debug, Default, ProtoBuf)] +pub struct GroupRowsChangesetPB { + #[pb(index = 1)] + pub group_id: String, + + #[pb(index = 2)] + pub inserted_rows: Vec, + + #[pb(index = 3)] + pub deleted_rows: Vec, + + #[pb(index = 4)] + pub updated_rows: Vec, +} + +impl GroupRowsChangesetPB { + 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() + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs index 7aa3a1a43e..f5daa803bc 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/mod.rs @@ -1,7 +1,9 @@ mod board_card; mod configuration; mod group; +mod group_changeset; pub use board_card::*; pub use configuration::*; pub use group::*; +pub use group_changeset::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index 84bd635b71..adb016ed41 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -1,7 +1,8 @@ use flowy_error::{FlowyError, FlowyResult}; use crate::entities::{ - CreateRowParams, GridFilterConfiguration, GridLayout, GridSettingPB, GroupPB, GroupRowsChangesetPB, RowPB, + CreateRowParams, GridFilterConfiguration, GridLayout, GridSettingPB, GroupPB, GroupRowsChangesetPB, InsertedRowPB, + RowPB, }; use crate::services::grid_editor_task::GridServiceTaskScheduler; use crate::services::group::{default_group_configuration, Group, GroupConfigurationDelegate, GroupService}; @@ -91,8 +92,6 @@ impl GridViewRevisionEditor { } }, } - - todo!() } pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { @@ -100,7 +99,11 @@ impl GridViewRevisionEditor { match params.group_id.as_ref() { None => {} Some(group_id) => { - let changeset = GroupRowsChangesetPB::insert(group_id.clone(), vec![row_pb.clone()]); + let inserted_row = InsertedRowPB { + row: row_pb.clone(), + index: None, + }; + let changeset = GroupRowsChangesetPB::insert(group_id.clone(), vec![inserted_row]); self.notify_did_update_group(changeset).await; } } @@ -120,7 +123,7 @@ impl GridViewRevisionEditor { async fn group_id_of_row(&self, row_id: &str) -> Option { let read_guard = self.groups.read().await; for group in read_guard.iter() { - if group.rows.iter().find(|row| row.id == row_id).is_some() { + if group.rows.iter().any(|row| row.id == row_id) { return Some(group.id.clone()); } } @@ -167,7 +170,7 @@ impl GridViewRevisionEditor { } async fn notify_did_update_group(&self, changeset: GroupRowsChangesetPB) { - send_dart_notification(&changeset.group_id, GridNotification::DidUpdateBoard) + send_dart_notification(&changeset.group_id, GridNotification::DidUpdateGroup) .payload(changeset) .send(); } diff --git a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs index 77673470e3..2fd4bed686 100644 --- a/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/group/group_service.rs @@ -1,8 +1,6 @@ -use crate::dart_notification::{send_dart_notification, GridNotification}; use crate::entities::{ - CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, GroupPB, GroupRowsChangesetPB, - NumberGroupConfigurationPB, RowPB, SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, - UrlGroupConfigurationPB, + CheckboxGroupConfigurationPB, DateGroupConfigurationPB, FieldType, NumberGroupConfigurationPB, + SelectOptionGroupConfigurationPB, TextGroupConfigurationPB, UrlGroupConfigurationPB, }; use crate::services::group::{ CheckboxGroupController, Group, GroupActionHandler, MultiSelectGroupController, SingleSelectGroupController, From 162152916c4849d569b5c0f7d25a50762d03367e Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 17:36:39 +0800 Subject: [PATCH 163/224] chore: delete board card --- .../lib/plugins/board/application/board_bloc.dart | 8 -------- .../lib/plugins/board/application/card/card_bloc.dart | 11 +++++++++++ .../lib/plugins/board/presentation/card/card.dart | 7 +++++-- .../presentation/widgets/row/row_action_sheet.dart | 7 +++++-- .../src/widgets/board_column/board_column_data.dart | 7 +++++-- .../rust-lib/flowy-grid/src/services/block_manager.rs | 1 + 6 files changed, 27 insertions(+), 14 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 8afdb15051..ed15c9703c 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -138,14 +138,6 @@ class BoardBloc extends Bloc { List _buildRows(List rows) { final items = rows.map((row) { - // final rowInfo = RowInfo( - // gridId: _dataController.gridId, - // blockId: row.blockId, - // id: row.id, - // fields: _dataController.fieldCache.unmodifiableFields, - // height: row.height.toDouble(), - // rawRow: row, - // ); return BoardColumnItem(row: row); }).toList(); diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart index 1f66ebe335..fe66825cef 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -58,6 +58,17 @@ class BoardCardBloc extends Bloc { return super.close(); } + RowInfo rowInfo() { + return RowInfo( + gridId: _rowService.gridId, + blockId: _rowService.blockId, + fields: UnmodifiableListView( + state.cells.map((cell) => cell._field).toList(), + ), + rowPB: state.rowPB, + ); + } + Future _startListening() async { _dataController.addListener( onRowChanged: (cells, reason) { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 2a4006e68c..48c3f6e5d9 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -1,9 +1,10 @@ import 'package:app_flowy/plugins/board/application/card/card_bloc.dart'; import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_action_sheet.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; -import 'package:flowy_sdk/log.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'card_cell_builder.dart'; @@ -85,6 +86,8 @@ class _CardMoreOption extends StatelessWidget with CardAccessory { @override void onTap(BuildContext context) { - Log.debug('show options'); + GridRowActionSheet( + rowData: context.read().rowInfo(), + ).show(context, direction: AnchorDirection.bottomWithCenterAligned); } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart index 8296add94e..b4390d098f 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart @@ -53,7 +53,10 @@ class GridRowActionSheet extends StatelessWidget { ); } - void show(BuildContext overlayContext) { + void show( + BuildContext overlayContext, { + AnchorDirection direction = AnchorDirection.leftWithCenterAligned, + }) { FlowyOverlay.of(overlayContext).insertWithAnchor( widget: OverlayContainer( child: this, @@ -61,7 +64,7 @@ class GridRowActionSheet extends StatelessWidget { ), identifier: GridRowActionSheet.identifier(), anchorContext: overlayContext, - anchorDirection: AnchorDirection.leftWithCenterAligned, + anchorDirection: direction, ); } diff --git a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart index 847f07c86e..4a04f4b662 100644 --- a/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart +++ b/frontend/app_flowy/packages/appflowy_board/lib/src/widgets/board_column/board_column_data.dart @@ -51,8 +51,11 @@ class AFBoardColumnDataController extends ChangeNotifier with EquatableMixin { return item; } - int removeWhere(bool Function(AFColumnItem) condition) { - return items.indexWhere(condition); + void removeWhere(bool Function(AFColumnItem) condition) { + final index = items.indexWhere(condition); + if (index != -1) { + removeAt(index); + } } /// Move the item from [fromIndex] to [toIndex]. It will do nothing if the diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index e6ada307db..253ce65e8b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -122,6 +122,7 @@ impl GridBlockManager { Ok(()) } + #[tracing::instrument(level = "trace", skip_all, err)] pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let row_id = row_id.to_owned(); let block_id = self.persistence.get_block_id(&row_id)?; From 35d5bc92ac403fa66c8de5cd049e2afc7fcfeae1 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 18:14:09 +0800 Subject: [PATCH 164/224] chore: publish preparation --- .../packages/flowy_editor/.gitignore | 1 + .../app_flowy/packages/flowy_editor/README.md | 19 ++++--- .../packages/flowy_editor/coverage/lcov.info | 0 .../flowy_editor/documentation/node.md | 51 +++++++++++++++++++ .../flowy_editor/lib/flowy_editor.dart | 1 + .../lib/src/service/input_service.dart | 27 +++++++++- .../lib/src/service/keyboard_service.dart | 26 ++++++++++ .../src/service/render_plugin_service.dart | 2 +- .../lib/src/service/scroll_service.dart | 27 ++++++++++ .../lib/src/service/selection_service.dart | 6 +-- 10 files changed, 147 insertions(+), 13 deletions(-) delete mode 100644 frontend/app_flowy/packages/flowy_editor/coverage/lcov.info create mode 100644 frontend/app_flowy/packages/flowy_editor/documentation/node.md diff --git a/frontend/app_flowy/packages/flowy_editor/.gitignore b/frontend/app_flowy/packages/flowy_editor/.gitignore index d920ae6823..7501b909b4 100644 --- a/frontend/app_flowy/packages/flowy_editor/.gitignore +++ b/frontend/app_flowy/packages/flowy_editor/.gitignore @@ -28,3 +28,4 @@ migrate_working_dir/ .dart_tool/ .packages build/ +coverage/ diff --git a/frontend/app_flowy/packages/flowy_editor/README.md b/frontend/app_flowy/packages/flowy_editor/README.md index 1e23b05409..4f104fb3a2 100644 --- a/frontend/app_flowy/packages/flowy_editor/README.md +++ b/frontend/app_flowy/packages/flowy_editor/README.md @@ -15,7 +15,10 @@ and the Flutter guide for

An easily extensible, test-covered rich text editing component for Flutter

-![](documentation/images/example.png) + +
+ +
## Features @@ -77,16 +80,16 @@ flutter run ## Examples * 样式扩展 - * Checkbox text - 展示如何基于已有的富文本组件扩展新的样式, - * Image - 展示如何扩展新的节点,并且渲染 + * [Checkbox Text](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart) - 展示如何基于已有的富文本组件扩展新的样式, + * [Image](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart) - 展示如何扩展新的节点,并且渲染 + * 更多请参照 [rich text plugins](https://github.com/LucasXu0/AppFlowy/tree/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text) * 快捷键扩展 - * BUIS - 展示如何通过快捷键对文字进行加粗/下划线/斜体/加粗 - * 粘贴HTML - 展示如何通过快捷键处理粘贴的样式 + * [BUIS](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) - 展示如何通过快捷键对文字进行加粗/下划线/斜体/加粗 + * [粘贴HTML](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) - 展示如何通过快捷键处理粘贴的样式 + * 更多请参照 [internal key event handlers](https://github.com/LucasXu0/AppFlowy/tree/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers) ## Glossary - - - +我们正在编写更详细的说明,目前使用请查照API文档 ## Contributing Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. Please look at [CONTRIBUTING.md](documentation/contributing.md) for details. \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info b/frontend/app_flowy/packages/flowy_editor/coverage/lcov.info deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/app_flowy/packages/flowy_editor/documentation/node.md b/frontend/app_flowy/packages/flowy_editor/documentation/node.md new file mode 100644 index 0000000000..00c4d7ac88 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/documentation/node.md @@ -0,0 +1,51 @@ +# Node + +Node is the data structure used to describe the rendering information. + +## JSON + +FlowyEditor uses a specific JSON data format to describe documents. + +![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7bd4be04-82c8-458c-9321-a5ed79aa94ed/Untitled.png) + +Each part of a document can be converted by the above format, for example + +![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/930ae42a-055f-4d79-bc83-921fa7845b4d/Untitled.png) + +An outermost layer is an object whose type is ‘editor’, which is used as the entry for FlowyEditor to parse data. And children are the details of the document. + +## Node + +The state tree is an in-memory mapping of a JSON file, consisting of nodes**,** and its property names are consistent with JSON keys. So each node must contain fields for **type, attributes, and children**. + +![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/04986ead-9bb6-4f32-9584-190bd70ce5e8/Untitled.png) + +### **Type** + +Type is an identifier describing the current node. The render system distributes to the corresponding builders according to the type. Note that nodes whose type is equal to ‘text’ are used as internal reserved fields. + +### Attributes + +Attributes is an information data describing the current node. We reserve the **subtype** field to describe the derived type of the current node, and other fields can be extended at will. + +### Children + +Children are the child node describing the current node. We assume that each node can nest the other nodes. + +We encapsulate operations on Node, such as insert, delete and modify info StateTree. It holds the root node and is responsible for converting between JSON to and from it. + +### Path + +Path is an array of integer numbers to locate a node in the state tree. For example, [0, 1] represents the second child of the first child of the root node. + +### Selection + +### Reversed field + +**Type** + +- text + +**Attributes** + +- subtype \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index f134d309b0..6f13228474 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -1,3 +1,4 @@ +/// FlowyEditor library library flowy_editor; export 'src/document/node.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart index da755ab346..2ca2a723d5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/input_service.dart @@ -7,13 +7,38 @@ import 'package:flowy_editor/src/editor_state.dart'; import 'package:flowy_editor/src/extensions/node_extensions.dart'; import 'package:flowy_editor/src/operation/transaction_builder.dart'; +/// [FlowyInputService] is responsible for processing text input, +/// including text insertion, deletion and replacement. +/// +/// Usually, this service can be obtained by the following code. +/// ```dart +/// final inputService = editorState.service.inputService; +/// +/// /** update text editing value*/ +/// inputService?.attach(...); +/// +/// /** apply text editing deltas*/ +/// inputService?.apply(...); +/// ``` +/// abstract class FlowyInputService { + /// Updates the [TextEditingValue] of the text currently being edited. + /// + /// Note that if there are IME-related requirements, + /// please config `composing` value within [TextEditingValue] void attach(TextEditingValue textEditingValue); + + /// Applies insertion, deletion and replacement + /// to the text currently being edited. + /// + /// For more information, please check [TextEditingDelta]. void apply(List deltas); + + /// Closes the editing state of the text currently being edited. void close(); } -/// process input +/// Processes text input class FlowyInput extends StatefulWidget { const FlowyInput({ Key? key, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart index b7573e4ed6..9f3a7be761 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/keyboard_service.dart @@ -3,9 +3,35 @@ import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +/// [FlowyKeyboardService] is responsible for processing shortcut keys, +/// like command, shift, control keys. +/// +/// Usually, this service can be obtained by the following code. +/// ```dart +/// final keyboardService = editorState.service.keyboardService; +/// +/// /** Simulates shortcut key input*/ +/// keyboardService?.onKey(...); +/// +/// /** Enables or disables this service */ +/// keyboardService?.enable(); +/// keyboardService?.disable(); +/// ``` +/// abstract class FlowyKeyboardService { + /// Processes shortcut key input. KeyEventResult onKey(RawKeyEvent event); + + /// Enables shortcuts service. void enable(); + + /// Disables shortcuts service. + /// + /// In some cases, if your custom component needs to monitor + /// keyboard events separately, + /// you can disable the keyboard service of flowy_editor. + /// But you need to call the `enable` function to restore after exiting + /// your custom component, otherwise the keyboard service will fail. void disable(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart index b6f9414a9e..c681c3283e 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/render_plugin_service.dart @@ -17,7 +17,7 @@ abstract class FlowyRenderPluginService { /// Register render plugin with specified [name]. /// /// [name] should be [Node].type - /// or [Node].type + '/' + [Node].attributes['subtype']. + /// or `[Node].type + '/' + [Node].attributes['subtype']`. /// /// e.g. 'text', 'text/checkbox', or 'text/heading' /// diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart index 199249b715..a7bbd8256f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/scroll_service.dart @@ -2,18 +2,45 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/src/extensions/object_extensions.dart'; +/// [FlowyScrollService] is responsible for processing document scrolling. +/// +/// Usually, this service can be obtained by the following code. +/// ```dart +/// final keyboardService = editorState.service.scrollService; +/// ``` +/// abstract class FlowyScrollService { + /// Returns the offset of the current document on the vertical axis. double get dy; + + /// Returns the height of the current document. double? get onePageHeight; + /// Returns the number of pages in the current document. int? get page; + /// Returns the maximum scroll height on the vertical axis. double get maxScrollExtent; + + /// Returns the minimum scroll height on the vertical axis. double get minScrollExtent; + /// Scrolls to the specified position. + /// + /// This function will filter illegal values. + /// Only within the range of minScrollExtent and maxScrollExtent are legal values. void scrollTo(double dy); + /// Enables scroll service. void enable(); + + /// Disables scroll service. + /// + /// In some cases, you can disable scroll service of flowy_editor + /// when your custom component appears, + /// + /// But you need to call the `enable` function to restore after exiting + /// your custom component, otherwise the scroll service will fails. void disable(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart index a256d71f03..aea643af08 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/service/selection_service.dart @@ -40,9 +40,9 @@ abstract class FlowySelectionService { /// The result are ordered from back to front if the selection is forward. /// The result are ordered from front to back if the selection is backward. /// - /// For example, Here is an array of selected nodes, [n1, n2, n3]. - /// The result will be [n3, n2, n1] if the selection is forward, - /// and [n1, n2, n3] if the selection is backward. + /// For example, Here is an array of selected nodes, `[n1, n2, n3]`. + /// The result will be `[n3, n2, n1]` if the selection is forward, + /// and `[n1, n2, n3]` if the selection is backward. /// /// Returns empty result if there is no nodes are selected. List get currentSelectedNodes; From 5ee6875d7bed58544149effcdeeda914a6303f2d Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 18:02:39 +0800 Subject: [PATCH 165/224] chore: open card detail --- .../board/application/card/card_bloc.dart | 6 ---- .../board/presentation/board_page.dart | 36 +++++++++++++++++++ .../plugins/board/presentation/card/card.dart | 5 +++ .../presentation/card/card_container.dart | 15 +++++--- .../row/row_action_sheet_bloc.dart | 8 ++--- .../grid/application/row/row_bloc.dart | 5 ++- .../grid/application/row/row_cache.dart | 2 -- .../grid/application/row/row_service.dart | 15 ++++---- .../plugins/grid/presentation/grid_page.dart | 6 ++-- 9 files changed, 69 insertions(+), 29 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart index fe66825cef..ab6aeacfcc 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -23,7 +23,6 @@ class BoardCardBloc extends Bloc { }) : _rowService = RowFFIService( gridId: gridId, blockId: dataController.rowPB.blockId, - rowId: dataController.rowPB.id, ), _dataController = dataController, super(BoardCardState.initial( @@ -34,9 +33,6 @@ class BoardCardBloc extends Bloc { initial: (_InitialRow value) async { await _startListening(); }, - createRow: (_CreateRow value) { - _rowService.createRow(); - }, didReceiveCells: (_DidReceiveCells value) async { final cells = value.gridCellMap.values .map((e) => GridCellEquatable(e.field)) @@ -61,7 +57,6 @@ class BoardCardBloc extends Bloc { RowInfo rowInfo() { return RowInfo( gridId: _rowService.gridId, - blockId: _rowService.blockId, fields: UnmodifiableListView( state.cells.map((cell) => cell._field).toList(), ), @@ -83,7 +78,6 @@ class BoardCardBloc extends Bloc { @freezed class BoardCardEvent with _$BoardCardEvent { const factory BoardCardEvent.initial() = _InitialRow; - const factory BoardCardEvent.createRow() = _CreateRow; const factory BoardCardEvent.didReceiveCells( GridCellMap gridCellMap, RowsChangedReason reason) = _DidReceiveCells; } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index 1fb31abcc7..e7202e0a6d 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -1,11 +1,20 @@ // ignore_for_file: unused_field +import 'dart:collection'; + import 'package:app_flowy/plugins/board/application/card/card_data_controller.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_cache.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_cache.dart'; +import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../grid/application/row/row_cache.dart'; import '../application/board_bloc.dart'; import 'card/card.dart'; import 'card/card_cell_builder.dart'; @@ -123,9 +132,36 @@ class BoardContent extends StatelessWidget { onEditEditing: (rowId) { context.read().add(BoardEvent.endEditRow(rowId)); }, + openCard: (context) => _openCard( + gridId, + fieldCache, + rowPB, + rowCache, + context, + ), ), ); } + + void _openCard(String gridId, GridFieldCache fieldCache, RowPB rowPB, + GridRowCache rowCache, BuildContext context) { + final rowInfo = RowInfo( + gridId: gridId, + fields: UnmodifiableListView(fieldCache.fields), + rowPB: rowPB, + ); + + final dataController = GridRowDataController( + rowInfo: rowInfo, + fieldCache: fieldCache, + rowCache: rowCache, + ); + + RowDetailPage( + cellBuilder: GridCellBuilder(delegate: dataController), + dataController: dataController, + ).show(context); + } } extension HexColor on Color { diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 48c3f6e5d9..20640f5601 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -18,6 +18,7 @@ class BoardCard extends StatefulWidget { final CardDataController dataController; final BoardCellBuilder cellBuilder; final OnEndEditing onEditEditing; + final void Function(BuildContext) openCard; const BoardCard({ required this.gridId, @@ -25,6 +26,7 @@ class BoardCard extends StatefulWidget { required this.dataController, required this.cellBuilder, required this.onEditEditing, + required this.openCard, Key? key, }) : super(key: key); @@ -54,6 +56,9 @@ class _BoardCardState extends State { accessoryBuilder: (context) { return [const _CardMoreOption()]; }, + onTap: (context) { + widget.openCard(context); + }, child: Column( children: _makeCells(context, state.gridCellMap), ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart index ce8df101a5..abca27e5c5 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card_container.dart @@ -7,8 +7,10 @@ import 'package:styled_widget/styled_widget.dart'; class BoardCardContainer extends StatelessWidget { final Widget child; final CardAccessoryBuilder? accessoryBuilder; + final void Function(BuildContext) onTap; const BoardCardContainer({ required this.child, + required this.onTap, this.accessoryBuilder, Key? key, }) : super(key: key); @@ -30,11 +32,14 @@ class BoardCardContainer extends StatelessWidget { } } - return Padding( - padding: const EdgeInsets.all(8), - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30), - child: container, + return GestureDetector( + onTap: () => onTap(context), + child: Padding( + padding: const EdgeInsets.all(8), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 30), + child: container, + ), ), ); }, diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart index e586f65bd4..fa81e6cb23 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_action_sheet_bloc.dart @@ -17,19 +17,19 @@ class RowActionSheetBloc RowActionSheetBloc({required RowInfo rowInfo}) : _rowService = RowFFIService( gridId: rowInfo.gridId, - blockId: rowInfo.blockId, - rowId: rowInfo.rowPB.id, + blockId: rowInfo.rowPB.blockId, ), super(RowActionSheetState.initial(rowInfo)) { on( (event, emit) async { await event.map( deleteRow: (_DeleteRow value) async { - final result = await _rowService.deleteRow(); + final result = await _rowService.deleteRow(state.rowData.rowPB.id); logResult(result); }, duplicateRow: (_DuplicateRow value) async { - final result = await _rowService.duplicateRow(); + final result = + await _rowService.duplicateRow(state.rowData.rowPB.id); logResult(result); }, ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index 6716967e5e..bea25d8008 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -20,8 +20,7 @@ class RowBloc extends Bloc { required GridRowDataController dataController, }) : _rowService = RowFFIService( gridId: rowInfo.gridId, - blockId: rowInfo.blockId, - rowId: rowInfo.rowPB.id, + blockId: rowInfo.rowPB.blockId, ), _dataController = dataController, super(RowState.initial(rowInfo, dataController.loadData())) { @@ -32,7 +31,7 @@ class RowBloc extends Bloc { await _startListening(); }, createRow: (_CreateRow value) { - _rowService.createRow(); + _rowService.createRow(rowInfo.rowPB.id); }, didReceiveCells: (_DidReceiveCells value) async { final cells = value.gridCellMap.values diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index 74e11e409e..68c8b6f519 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -255,7 +255,6 @@ class GridRowCache { RowInfo buildGridRow(RowPB rowPB) { return RowInfo( gridId: gridId, - blockId: block.id, fields: _fieldNotifier.fields, rowPB: rowPB, ); @@ -283,7 +282,6 @@ class _RowChangesetNotifier extends ChangeNotifier { class RowInfo with _$RowInfo { const factory RowInfo({ required String gridId, - required String blockId, required UnmodifiableListView fields, required RowPB rowPB, }) = _RowInfo; diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart index e610734064..52af18b296 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_service.dart @@ -9,12 +9,13 @@ import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart'; class RowFFIService { final String gridId; final String blockId; - final String rowId; - RowFFIService( - {required this.gridId, required this.blockId, required this.rowId}); + RowFFIService({ + required this.gridId, + required this.blockId, + }); - Future> createRow() { + Future> createRow(String rowId) { final payload = CreateTableRowPayloadPB.create() ..gridId = gridId ..startRowId = rowId; @@ -43,7 +44,7 @@ class RowFFIService { return GridEventMoveRow(payload).send(); } - Future> getRow() { + Future> getRow(String rowId) { final payload = RowIdPB.create() ..gridId = gridId ..blockId = blockId @@ -52,7 +53,7 @@ class RowFFIService { return GridEventGetRow(payload).send(); } - Future> deleteRow() { + Future> deleteRow(String rowId) { final payload = RowIdPB.create() ..gridId = gridId ..blockId = blockId @@ -61,7 +62,7 @@ class RowFFIService { return GridEventDeleteRow(payload).send(); } - Future> duplicateRow() { + Future> duplicateRow(String rowId) { final payload = RowIdPB.create() ..gridId = gridId ..blockId = blockId diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index fe9b245567..2c6c1e2180 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -239,8 +239,10 @@ class _GridRowsState extends State<_GridRows> { RowInfo rowInfo, Animation animation, ) { - final rowCache = - context.read().getRowCache(rowInfo.blockId, rowInfo.rowPB.id); + final rowCache = context.read().getRowCache( + rowInfo.rowPB.blockId, + rowInfo.rowPB.id, + ); /// Return placeholder widget if the rowCache is null. if (rowCache == null) return const SizedBox(); From 2c2127e84ae47c2ed83d01ac011840cac568285b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 18:34:37 +0800 Subject: [PATCH 166/224] docs: add documentation for selectable --- .../lib/src/render/selection/selectable.dart | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart index bdea01b895..5d4f95ade9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/src/render/selection/selectable.dart @@ -2,33 +2,40 @@ import 'package:flowy_editor/src/document/position.dart'; import 'package:flowy_editor/src/document/selection.dart'; import 'package:flutter/material.dart'; +/// [Selectable] is used for the editor to calculate the position +/// and size of the selection. +/// +/// The widget returned by NodeWidgetBuilder must be with [Selectable], +/// otherwise the [FlowySelectionService] will not work properly. /// mixin Selectable on State { - /// Returns a [List] of the [Rect] selection surrounded by start and end + /// Returns the [Selection] surrounded by start and end /// in current widget. /// /// [start] and [end] are the offsets under the global coordinate system. /// - /// The return result must be a [List] of the [Rect] - /// under the local coordinate system. Selection getSelectionInRange(Offset start, Offset end); + /// Returns a [List] of the [Rect] area within selection + /// in current widget. + /// + /// The return result must be a [List] of the [Rect] + /// under the local coordinate system. List getRectsInSelection(Selection selection); - /// Returns a [Rect] for the offset in current widget. + /// Returns [Position] for the offset in current widget. /// /// [start] is the offset of the global coordination system. + Position getPositionInOffset(Offset start); + + /// Returns [Rect] for the position in current widget. /// /// The return result must be an offset of the local coordinate system. - Position getPositionInOffset(Offset start); - Selection? getWorldBoundaryInOffset(Offset start) { - return null; - } - Rect? getCursorRectInPosition(Position position) { return null; } + /// Return global offset from local offset. Offset localToGlobal(Offset offset); Position start(); @@ -36,9 +43,15 @@ mixin Selectable on State { /// For [TextNode] only. /// - /// Returns a [TextSelection] or [Null]. - /// /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return null. TextSelection? getTextSelectionInSelection(Selection selection) => null; + + /// For [TextNode] only. + /// + /// Only the widget rendered by [TextNode] need to implement the detail, + /// and the rest can return null. + Selection? getWorldBoundaryInOffset(Offset start) { + return null; + } } From 9456fbd57388f53ad84afdf2d6608997867909dc Mon Sep 17 00:00:00 2001 From: appflowy Date: Tue, 16 Aug 2022 20:34:12 +0800 Subject: [PATCH 167/224] chore: fix some bugs --- .../plugins/board/application/board_bloc.dart | 4 ++- .../card/board_select_option_cell.dart | 8 ++++- .../presentation/card/board_text_cell.dart | 11 +++++-- .../presentation/card/board_url_cell.dart | 32 ++++++++++--------- .../plugins/board/presentation/card/card.dart | 2 +- .../cell/cell_service/context_builder.dart | 2 +- .../widgets/cell/date_cell/date_editor.dart | 2 +- .../widgets/cell/number_cell.dart | 11 +++++-- .../flowy-grid/src/services/grid_editor.rs | 7 ++-- .../src/services/grid_view_editor.rs | 2 +- .../src/services/grid_view_manager.rs | 8 +++-- 11 files changed, 58 insertions(+), 31 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index ed15c9703c..c837e4346f 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -253,5 +253,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } @override - void updateRow(String groupId, RowPB row) {} + void updateRow(String groupId, RowPB row) { + // + } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart index d430f869c1..373bb3c850 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_select_option_cell.dart @@ -42,7 +42,13 @@ class _BoardSelectOptionCellState extends State { .toList(); return Align( alignment: Alignment.centerLeft, - child: Wrap(children: children, spacing: 4, runSpacing: 2), + child: AbsorbPointer( + child: Wrap( + children: children, + spacing: 4, + runSpacing: 2, + ), + ), ); }, ), diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart index 8cb5c4987e..2da156ded8 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_text_cell.dart @@ -37,9 +37,14 @@ class _BoardTextCellState extends State { } else { return Align( alignment: Alignment.centerLeft, - child: FlowyText.regular( - state.content, - fontSize: 14, + child: ConstrainedBox( + constraints: BoxConstraints.loose( + const Size(double.infinity, 100), + ), + child: FlowyText.regular( + state.content, + fontSize: 14, + ), ), ); } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart index 5493b0d45b..31cca41e6a 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/board_url_cell.dart @@ -35,22 +35,24 @@ class _BoardUrlCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - final richText = RichText( - textAlign: TextAlign.left, - text: TextSpan( - text: state.content, - style: TextStyle( - color: theme.main2, - fontSize: 14, - decoration: TextDecoration.underline, + if (state.content.isEmpty) { + return const SizedBox(); + } else { + return Align( + alignment: Alignment.centerLeft, + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), ), - ), - ); - - return Align( - alignment: Alignment.centerLeft, - child: richText, - ); + ); + } }, ), ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart index 20640f5601..a5c7b7ba2c 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/card/card.dart @@ -73,7 +73,7 @@ class _BoardCardState extends State { (cellId) { final child = widget.cellBuilder.buildCell(cellId); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), child: child, ); }, diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart index c8e92809d7..1068cbf36b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/context_builder.dart @@ -242,7 +242,7 @@ class IGridCellController extends Equatable { .getFieldTypeOptionData(fieldType: fieldType) .then((result) { return result.fold( - (data) => parser.fromBuffer(data.typeOptionData), + (data) => left(parser.fromBuffer(data.typeOptionData)), (err) => right(err), ); }); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart index 4ab3ff352d..f6ddf42fba 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart @@ -15,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:app_flowy/plugins/grid/application/prelude.dart'; - import '../../../layout/sizes.dart'; import '../../header/type_option/date.dart'; @@ -39,6 +38,7 @@ class DateCellEditor with FlowyOverlayDelegate { final result = await cellController.getFieldTypeOption(DateTypeOptionDataParser()); + result.fold( (dateTypeOptionPB) { final calendar = _CellCalendarWidget( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart index 004c34b657..2926972f95 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/number_cell.dart @@ -49,8 +49,10 @@ class _NumberCellState extends GridFocusNodeCellState { controller: _controller, focusNode: focusNode, onEditingComplete: () => focusNode.unfocus(), - maxLines: null, + onSubmitted: (_) => focusNode.unfocus(), + maxLines: 1, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + textInputAction: TextInputAction.done, decoration: const InputDecoration( contentPadding: EdgeInsets.zero, border: InputBorder.none, @@ -63,8 +65,6 @@ class _NumberCellState extends GridFocusNodeCellState { @override Future dispose() async { - _delayOperation?.cancel(); - _cellBloc.close(); super.dispose(); } @@ -76,6 +76,11 @@ class _NumberCellState extends GridFocusNodeCellState { if (_cellBloc.isClosed == false && _controller.text != contentFromState(_cellBloc.state)) { _cellBloc.add(NumberCellEvent.updateCell(_controller.text)); + + if (!mounted) { + _delayOperation = null; + _cellBloc.close(); + } } }); } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index c39bb9e929..215cd3c467 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -287,7 +287,7 @@ impl GridRevisionEditor { pub async fn create_row(&self, params: CreateRowParams) -> FlowyResult { let mut row_rev = self.create_row_rev().await?; - self.view_manager.update_row(&mut row_rev, ¶ms).await; + self.view_manager.fill_row(&mut row_rev, ¶ms).await; let row_pb = self.create_row_pb(row_rev, params.start_row_id.clone()).await?; @@ -314,7 +314,10 @@ impl GridRevisionEditor { } pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> { - self.block_manager.update_row(changeset, make_row_from_row_rev).await + let row_id = changeset.row_id.clone(); + let _ = self.block_manager.update_row(changeset, make_row_from_row_rev).await?; + self.view_manager.did_update_row(&row_id).await; + Ok(()) } pub async fn get_rows(&self, block_id: &str) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs index adb016ed41..6d1c231480 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_editor.rs @@ -76,7 +76,7 @@ impl GridViewRevisionEditor { }) } - pub(crate) async fn update_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { + pub(crate) async fn fill_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { match params.layout { GridLayout::Table => { // Table can be grouped too diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs index edae70ba63..b5f8d41691 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_view_manager.rs @@ -47,12 +47,16 @@ impl GridViewManager { }) } - pub(crate) async fn update_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { + pub(crate) async fn fill_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) { for view_editor in self.view_editors.iter() { - view_editor.update_row(row_rev, params).await; + view_editor.fill_row(row_rev, params).await; } } + pub(crate) async fn did_update_row(&self, row_id: &str) { + let row = self.block_manager.get_row_rev(row_id).await; + } + pub(crate) async fn did_create_row(&self, row_pb: &RowPB, params: &CreateRowParams) { for view_editor in self.view_editors.iter() { view_editor.did_create_row(row_pb, params).await; From 2224aa271e267377227293b1adbfec46d140e652 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 16 Aug 2022 18:34:37 +0800 Subject: [PATCH 168/224] docs: add documentation for selectable --- .../app_flowy/packages/flowy_editor/README.md | 32 ++++++----- .../documentation/contributing.md | 17 +++--- .../documentation/images/example.png | Bin 939770 -> 1339980 bytes .../documentation/images/reporting_bugs.png | Bin 0 -> 1326451 bytes .../flowy_editor/documentation/node.md | 51 ------------------ .../flowy_editor/example/assets/example.json | 20 +++---- .../lib/src/service/keyboard_service.dart | 2 +- 7 files changed, 36 insertions(+), 86 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/documentation/images/reporting_bugs.png delete mode 100644 frontend/app_flowy/packages/flowy_editor/documentation/node.md diff --git a/frontend/app_flowy/packages/flowy_editor/README.md b/frontend/app_flowy/packages/flowy_editor/README.md index 4f104fb3a2..3ba6afc045 100644 --- a/frontend/app_flowy/packages/flowy_editor/README.md +++ b/frontend/app_flowy/packages/flowy_editor/README.md @@ -11,20 +11,20 @@ and the Flutter guide for [developing packages and plugins](https://flutter.dev/developing-packages). --> -
FlowyEditor
+

FlowyEditor

An easily extensible, test-covered rich text editing component for Flutter

- +
## Features * Extensible - * Support for extending different styles of views. - * 支持扩展不同样式的视图 + * Support extending custom components. + * 支持扩展自定义的组件 * Support extending custom shortcut key parsing * 支持扩展自定义快捷键解析 * Support extending toolbar/popup list(WIP) @@ -34,11 +34,9 @@ and the Flutter guide for * All changes to the document are based on **operation**. Theoretically, collaborative editing will be supported in the future. * 所有对文档的修改都是基于operation。理论上未来会支持协同编辑。 * Good stability guarantees - * Current code coverage >= 60%, we will still continue to add more test cases. + * Current code coverage >= 63%, we will still continue to add more test cases. -> 由于可扩展的结构,以及随着功能的增多,我们鼓励每个提交的文件或者代码段,都可以在test下增加对应的测试用例代码,尽可能得保证提交者不需要担心自己的代码影响了已有的逻辑。 - -> Due to the extensible structure and the increase in functionality, we encourage each commit to add test case code under test to ensure that the committer does not have to worry about their code affecting the existing logic as much as possible. +> Due to the extensible structure and the increase in functionality, we encourage each commit to add test case code under test to ensure that the other committer does not have to worry about their code affecting the existing logic as much as possible. For more testing information, please check [TESTING.md](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/documentation/testing.md) ## Getting started @@ -79,17 +77,17 @@ flutter run ``` ## Examples -* 样式扩展 - * [Checkbox Text](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart) - 展示如何基于已有的富文本组件扩展新的样式, - * [Image](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart) - 展示如何扩展新的节点,并且渲染 - * 更多请参照 [rich text plugins](https://github.com/LucasXu0/AppFlowy/tree/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text) -* 快捷键扩展 - * [BUIS](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) - 展示如何通过快捷键对文字进行加粗/下划线/斜体/加粗 - * [粘贴HTML](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) - 展示如何通过快捷键处理粘贴的样式 - * 更多请参照 [internal key event handlers](https://github.com/LucasXu0/AppFlowy/tree/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers) +* Extends custom components. + * [Checkbox Text](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text/checkbox_text.dart) - Showing how to extend new styles based on existing rich text components. + * [Image](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart) - Showing how to extend a new node and render it. + * More examples. [rich text plugins](https://github.com/LucasXu0/AppFlowy/tree/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/render/rich_text) +* Extends custom shortcut keys. + * [BUIS](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) - Showing how to make text bold/underline/italic/strikethrough through shortcut keys + * [Paste HTML](https://github.com/LucasXu0/AppFlowy/blob/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) - Showing how to handle pasted styles through shortcut keys + * More examples. [internal key event handlers](https://github.com/LucasXu0/AppFlowy/tree/documentation/flowy_editor/frontend/app_flowy/packages/flowy_editor/lib/src/service/internal_key_event_handlers) ## Glossary -我们正在编写更详细的说明,目前使用请查照API文档 +We are working on more detailed instructions, for now please refer to the API documentation. ## Contributing Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. Please look at [CONTRIBUTING.md](documentation/contributing.md) for details. \ No newline at end of file diff --git a/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md b/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md index 8e264844ef..d37531e196 100644 --- a/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md +++ b/frontend/app_flowy/packages/flowy_editor/documentation/contributing.md @@ -1,14 +1,17 @@ # Contributing ## Reporting Bugs -补充截图,在 Appflowy 仓库增加一个 editor bug 的截图 +Please click this [link](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=bug,editor&template=bug_report.md&title=%5BFlowyEditor%20Bug%5D+Untitled+Bug+Issue) to report bugs. -## 技术讨论与支持 -补充discord截图,editor那个群 +![](../documentation/images/reporting_bugs.png) -## 提交PR -我们很欢迎和appreciate大家提交的PR。我们也有很多first-contributor-welcome or help-wanted 的 issue 欢迎大家一起来实现。 +## Technical Discussion And Support +Discord link ... -BTW: 正如ReadMe所说,我们想保证大家提交的代码不会影响到现有的代码逻辑和功能,所以每次提交PR请附上对应的test,建议在test加上对测试用例,范围的简单描述。更多细节请看test.md +## Submitting Pull Request -最后,重复一句,由于我们是社区驱动的开源编辑器,所以我们会认真对待每一个PR以及每一次的PR,非常感觉大家的贡献。 +We welcome and appreciate your pull request submissions, and we have many good-first-issue-for-devs or help-wanted issues that we welcome you to implement. + +As [README](../README.md) said, we want to make sure that the code you submit will not affect the existing code logic and functionality, so please attach the corresponding test for each PR submission, and it is recommended to add a brief description of the test case and scope to the test. For more details, please see [TESTING.md](./testing.md) + +Finally, to repeat, since the FlowyEditor is a community-driven open source editor, we take every PR seriously and feel very much for everyone's contribution. diff --git a/frontend/app_flowy/packages/flowy_editor/documentation/images/example.png b/frontend/app_flowy/packages/flowy_editor/documentation/images/example.png index a810f0dae514a6cd37f51242056bdc9f55043fe8..a13548b7c19e73b77a3952708c276bfc664a3709 100644 GIT binary patch literal 1339980 zcmeGEc|4TuA3lt0)!n9@5|boL%Y-|^n5k6SG?oe#Q%#av$i9TD-W#^>on$?xvuk^xz6=Cj`wl8 zV|&bc$-;FDm6Vj096GqqUP)=8pOTWY(t`Qmoe3PiT1jb^tA~}9?IA0x4Yqzh7dfFnM9aCz|8uN_PO{`N3q)lPm^O6kRBQg}j)T6Rm`l|C=dS#u(x~MLBH{ryRMckJL z@OEC_!^}A`*1Jj{wjS+W<^63?ug@!Kr5kpK*~y_+d;F&L%W75yJwN2OM{L)jk>9v- z^`m12&&j3_=-te;;nI-&=W&NNUs=AIw5*=CYfp}v)V<#E;UBMJ>(`kd*>vz;(vrwS zf7;jHx_QBNq+#1woi^_w1(tszUuMt0?K;mqvgFsA^_IK+_qUfVZP9!i(!R6BA z_RIFAAS>S=4rT(Ae1GDs$=FGf5TS+O(Luu|m*Bk@Sik}4VP~`djJm=nR z(4I<4E5IY<@$7%EUg-CD&cDZs(@IKv9IOr<0?!UaKQ}jT|4Tjr8X0r?z#D&DIq2-K zq_jpy@tAeUe$yl__=K`*}h}@j*Zxb8#Zjf_+7YYZoluq zKiR=6%Z--;0Df72Etny#vmLZ1eWl{$0qw zP=+;4`p>re;Q%nGFB0Q~&+Q|HgO^rhoo#*8e&6 z|IB*A-_6g;#|wOD0QSEP_D|;jJ^4>Y3|evT|5F#gD|+T#P}A6j81#Qy4ZCpp$5o1s zx87qP{y2CB-AwT_=NkB<_xl+fFMPS4YRpSgQrfL_Xy2aWA+yG+PZxRH1kuZT9fH-% zdwRZh+&+8b=#A)Qe=OCtzP3{T=Q6Xq&o-%^*`J`25PtKZWcEAKMnBaj8wMXQ%l^4Z z%}?pvl`9De(adG)#LDisl_NK8D7kGu?b8L$o)au*w)54ZYGE%&sxCYexb8&OD9$?0 z@I_{)w7TN%-VJmA&+q@Kf&ZV?fTr^G^1>*sh^tKwuIygtu0D%5MO8z6S!6f+gJuH- zNe$*yPZwVB&yP5{#RMs^=z$O729;1MQRfJ|CFDDEpt^nO=QIY(wj-5n8`u=N`V+-= zH6$ZnkRd1-_p#<0kFru3zI(^ylcFFvn4T{4fkkg|?bYY8NIJM|BJ%HuS%Z-7QrYU2 zUbh32=@l|gp{ax+T?AIZ}e_S{9%qU9_-#Jo;)>m?%zqa7@n~ z@?p+*UH0J4lec$>LR~3SbFX6w?(r*5Y9Pm!Q{#)e;{x7IN8VNYbfCXhMEx|K(6$#X ztLjAAzKzpdPC*$EW~u<|*#G*<5D_hqD2 z6m>!19(-WytJlVz#%~GQ5^12hf6TLAP@MqU`wzoe+M4phJgLD<3lBkM%1)#ou1&FXixWU|V?&_0!Pj^YO_|`1ah-nY1j-g2Y z_*be^t36N2Tx4G~xsMFX+z`(Q7O4f*5!Zssz{M85S}}c3abyl1rHsJCvTvY-hMy}l za6yDT+NN>Y4+-1G40@7px8U#H3ob91q-|CIW!s0VJgUjMSUIbBJZI|J7hz)0a0hf^9Cm&y(lg9~0KC8(--5%6v3&JO?x% zey0C?yU{Xr$&^o7nJV+)T65{%TS`~9t2x?jhYjH4xT&JiC=YOtrWH>jUk2^#d0Z$a zZk%%r4E%Xe8M^(Fqu$Mw@zK=roY4p4>|Jl{Qo)CPrw9o`>n+9Is-C4=l3(L2uw9N^ z!yolsHT{>=13MXajtWhs>GB_ke}AFDF-|;5cq060S@D~qU0nxUguxq=@c*xS0J{CGaZxvsWi{CaUr{OaT2iv;(_wK-HPO)7kJy*OQ-=JZ^B^a!Z&tZjC> z?ee{m1?87fp^ln7TcLaZd&U^8c5ja@p#;~70$;CqLY3!P?Ivxk%sq~-!NQI4xzld|agvXE`dzdklJ? zk77zFqV8`pJX|i$66cFwiyfdlqOI@*=-Qs7eA3k{J}cgaRiv@VZ+KyL%g-`USQ9c4 za@_^iMf~YCO>oJPocIe+@YY5*NLN!rmdX$Qr>IKtX;9eM?@Tfm z+A!BufA%_M7009Vwfzcvo4N}5E=yh=SfE*d>(9e&oyV$~o1lh;;Vpf$FD8snPTTru zV*37oY!4w_P^Alz^cMZoI=LWAenx(&t1(EVu74q|7%T@lzqAUo&u@!QE=ck@?iJ{1 z8JHliOa+=5;w~@B;6SR{(&g>s$Q$F*z zj#E7B+EfpZ#sqd!?tJ5-#n)EnNwTnRNN~goT=5F^(Y&PZ6qlm++49_rKac&;w&#P< zohWUx+`PT?Zi-j?mrQA-OT#NK8;z*8D!=xc33n+qU3O>Z+i2BLZb_kB#ejA%Y~Np4 zdHSw`iY)A%$4+ISk2T0|#6L&pFU_;?e4&~65Eq{t?~I2+yP-r*QiDcD0)*fwsVKv(F@P&2u5Q&*bY;PsFcE8lVb&o8!R{pwD zPm1&11qY%M9|n^PI^L1T&^}VM6lzlq*cag(c(BLWK@i<0^Th;y4x4WFOb^Vq<-JCR zmBMDbA-YQ&C;(^hCtk0%5GBb6BV0se{?;X+IcCj0`RV-*Z}AT$+oIwfK9z3Yf4q7* zzpkdutY&`to4~Ez1*atmvoxf(x9{P6msPG-;Vx&ac9irL_f(|##B0K5wXtXSYM$b5 zrrh^ZWvr(E&Y?Ev>6f79?EI<$M!i*dZE#&+-S;5&No@T!fJYs0ZOY$;D7~?Y;{H9B zZX$X`(aEh|<=%tD#n0vC#I2gyTY3lRCFA{-{RZum9o{`uYl~HIUs2?9mF)bQXHYqJ z8S4L@i4On&N14@m(+%cN>C0xNuy;P{$_+cLc7s&7{Fg=qM)Og96g{xi!CF0h-Tcv) z!HZkJl`eOyKZlv77OkgxqS&}5o^ug{ zVMg!a43{8NMe^V-*c~!i`B1L0V*tvbeMYeAxMg^?;0G-k#_Ske5bCr{@Dd(?_>{3l zjsAyNzdKZT>-a{r>tsb_`=QL8iF+Fat*I4(@=1z?rj#_c zL$N73yo8H3eV#^R(6rV8f}W)%xy&0Kx08G)SDb+CuGht%fM?+PXK}}Y9U{h zVcyg(=|jI-oxMWRf~q8pgBpmjRNU+Rf9|!ngwLALEl$K_;4GjZcuaO@X#hL{Kar+R z5GtTf8p+u25p*ZD0vdt;uuN!#9!@ZvL^|vS(p$%aPnePz^RDpW*Zm{!R%4@08h#oS zvXAv41_Ktk-yN@AJ1ktd4;tNHGOP2Ex4BYZgsp4$bHbh7Hxak^`PWRQcY6oKDYqk!bkiAKDGj*{@`zbyH#bf5J3lQn`ZG{Na( zk)WNWSWje$mGOegthiZ8Gfn-v0W}dPvi0v;F8}X$v&@!AC~y zgiD{W*s!}-J4(PXky4(^v<$*J7fi0mN{vxcpa0z>Avo!+-~kB1?kOI>UV1fcA6xQGjp!Uc4}NUWuBsSeAnS6S~2{2 zX}E##WUGbm*059XRGl6dcKJ3*9~sW*MaFS2j{>Y;DP0$_H0&G}F)nUIo~~XQ(@*M0 z6OWGnU2(noRRO@qij<_b$nw3Wn$K!TWycy!?CXPC<1YktD2wfNs7lfa{K&5TmVe8A1&L%WyNlbp}1r6uxjo}d+SfMt3sT&s~}bAYW)@R%d7P!!ejr`>`d*# z??c#ucW`l>_5gufyE^hMs7XYaVw$m#=TYk+n>>2aDGD{$5>$hdd06<|pOXfYr#7p+ zJM=lJYiRzJPaPLg4bL($@?on=H+#v%Jv?sl$`QL7L6(PSR;<>QGoy)zGI-kr z{(6_LBPvo<#s8)rib!fjp^W)Xt;4@IjV&)fdsSZ*!tXm-We)vBY<2yv zw&kLlI}Ds*@Pf)u6`6E+$NUHDnNTx3^1yr*uX}?>2D^)<%$6@3W{tU)U%#gIV&K}| z-jdLi^q@QDnsIjo*;7f4pK@3Jjq-b)=$IKwU8gNgDwO=V_Mvh`_&t8*k(%^$>{MBJ za%gIJyPoB7?Dfs5_^*5_xthfo;9NSxgFoSb?lS&{89K(OVdgA({S!{Fnb?jb0or>3 zD#2~Am5IW+QQ8`zdkDE9k#q9*Ve?Yh$L1GK1Xci)|l{=Qdl>c#uECZI;lCb0Eh zr)?#T-7mjf;k`6OuXVFNGQ9gz@M2zYQ?TY_cSEvC!^KBgv#xyM?V<#3owh6szff^o zF?j~}!d0N9fc$WmAjeniPoOcPSCKYah3zQ##tiGcQ2 zUX2b1%P`%co)D?Le_{2~Y0-xVQCH8$Q64e%kt6C=yo8Us&S+X!;non&DGG7d#gTan zUZVSE3!UcAHW0tN)*6=bz~}u@@G71ZwkQy`BNsE_`w)6CQ)sgoUs{5niy>sKQ*WK#Drd-bV%fB-F z`ii~n`L)^JvmM0oZynw&o#!ew&Pl(CQ9pbWSE8;l_{j&iR7rEJdX~yq+>l@ z(_)F5nmkX0mBz>jiF4cFxU>fE!Xk3KnPlrMUNS~CG^c1N^rYN9$&@&VJS8^F! zG?-wj3_!?$EV>~jP}l1*f)IzKB52q-i_)E1ReQ9nxu0Sxh4aPbAO)AZ*FpbHTW0q_ zy|BDe4AsjsAakUuiGd4%*|PFXMBMP}E`%mQf}7d3v6!I`c1C!MP@#r|)dNjwZ|1r< z*Br;n$BN`e8aAekio~dRF0Bjln-U$HtF)s+{krx6y|n{5&M7TltX^!a%=^VO`+;|L z2-!4f_a?*KWPTnqslxf-V3~$%ra#XS%ffY(W3%rW8Gaq=gDy+*G5l=m@fy#t6TG@@ z5>h@?;Y4^a^7eM5tu9)%BU&2PsEdDUPa;f@hBd91kc2E(0ed z1-VYabq>}nbmFjV-t5eGRfqx5Gyga78jyaP<;jn@6{g&mp@tiG#@};4?zJyehdQ|i z>Zv#V&s5vGbolg1%2OY$!9-nzKPu-;U*BYSqxR~lJG}fUr19d?j#DgNNedE7dwp-v z!8e(|;Y2z52UK6CBG(yAJ4LA2pmgP1=-gWCdHd6T?jG1?JbU35J0AisfokKLHA^Kl z)I*!np5zGswR?LC9@Vhn7U&mob5v9aw9`n*;1>VrDrdx@+1g~_%sr%(13d+!f{|a0 z8)59*AU}e9y)Glb9DODQtQD$rXQB_k*9MPkQc+HqwGb`P203#6j)XoFb!x3wj(haW+zS_G4 zZMeo07iV8nyKt)Lh8^w zo(I8nt6K=!v4-zZgDS$N$P5MP!A%wSa11vwzB;6HnhcjAuOd1rBaUGGBb>h>qj|54 zE{<*1w1BBI^P$wW`9DCYXY(2W3sytNC{8l@TMuZ{W97bTL9nxs!v;_Z`5gNimVxYT-H-oYUS%ciT z=4&)U?1e16XCh{b_#24k0uVqTS4+uBodm=al&7oP`0`*#pp!fz3W1vS(YP@`hcp*tcCE4?YCUu2CZ^hx; zCD+NcQ#rIQe5PHsK({QO+h~<-E%B6E4H#$gACrxKE}#bYP7*ef4bd)0a{?Of_d65i zX=^ek?y?Aff2su@FSZuqq+~e_CTeTRR0Cqu z3tC+-S;g@^tVujbLZSD`-c3to5SR8srE7aD7zJn~N|}bJB1bLt;B*r^$}EGk_A(1* zc!WC_nXs@x6ti2mq@2$g=U-%TxpU&N!!T$%~KCpKK1px0h`lSf1?MR>nat&?oIT*rym2!< z3iH9p&9rP`?(C0N8R6&iHhw?W8&y)3{oNNMNo*eaMX1l7NDl8BBp$85L4%xIi;vRd zc54KrhBs6k$1*#ks{Cv?_c%Wtd!q`G{R7>m5`J}_qslj&Vfa=#TaLSoL?QfcRIw+R z^}bynV|-$&h-`(ZO`2$Ku8r!$RpV1^$fTMd?Oukp-~k8sg6v8)oL~M z;mkKBia1^iO6qc-HT8p3D48G!zch!QZtI~01KMpR0`xKf9DG{0B@mf>B~?Cur1AAg zo{V2hW?T4PZjH)B)SBPYruNHm%w6qyJB#?ZvPf1yDXz_-y#Ma{A!%Or>H)ltQu9;0 z#Rmtqib|eeX*;wf$ot`m;$Qw-L-qY<1+OPXwpn>=Dga~6q{~K>X8q8j;6aO`D5_(x z+Ft-v?~Si4B)`d$#f8d>af3(NRggSED+;Wc1i^F?LyAYbkxq(-a%LBzh+Y3fBo@t< zkUbW4sEF%RBj9G_DG0@laoTPIjUuW#LH-3yt6-g=cq6c3^#D^2+se$AZv#u_?CT)X zsYslEC)JRi7F0buTSWpdgdG?tvtFxy!ZhZ7c>lIxEnXjvT8_Nq(0)+@L z#t%b0FiLC6{bF`6_>6rfUZdb_c-I81|f|cgf+%IKc=aYFJt=H~B%O!Yh@{iI>oBk;r-({KMlSXFHLp8yk&%p$rfrM zd0I--rHO~%6?Ugm3>F_=%z5!^s<`S+UUvVT{)f3X_D@pE+dpwS!lNZ0LTt}8Xm-X8 zygO3kZyis?vmCXMw~<5q1VN?ikcx9nfZSS=j5>|=JjIaQMI0Kf5AZ~ms&FOW2B{x< zo5L!Gy3`w9EiMFOh7Mby9)ng_mhOI(D0j0yN`)t+dygIzXiO*Vgv*abHiIdI^J3BH zvaowdr^s38*vt)QL4lv`wt^K2m%sf2fQye+Y!!4J8kV(6io~AAce=?fluDPMb|W&+ zu?v_V5(tqy9Bor=R;D}HJ2#oxsRTcjgD`~na=SurQZWy@V$X+~FTZanT4y)hE> z-TGs{D8LbGh4M z5jr&55G{cZxk_7U=FFRw?X)y|=K&q|L?v_$#;6jN4-2L$OD{dcCE?VxL6m>z?UT6&Id>`D(1ten>NmcMnqn62L5TsrYbU%{$-`ES#o+n})z~LWg=T(5K1PH#B(13{EPn-4e z@dQrTQ7pcisk4hXZ2FZX2!>lwV-Mf?)IR0YYocmzmx}!v)?oQEfj_c{&6>tAGh;99 zHIlYy$`;8#I&MTfvk@_DC*i*iX_4hi>>!5xru;pldN(6b9{a&5{)klfp3{_WhEo>% z3_%aAR&osMiVbz>($7D0-};Af_~iSTd&O%Ti)3vM%K8j*sR2DXs4L|8>4w+G(&BxO zVHeV>HiT#m8qX*B*`#oWTnCF}JhM~>wqouQ*rcK-a#H&dC$?ju2QRmmk&E;pFXf?` z@}l0f9B71m)#pcU7UBWi2!L9>K&RHDx-?or7t@u#W?u`Uw?(APh!#F%3LhtpxE?h7 zXd~GIVF%mrDV5_c0)454uU*kUT!6Ko0|_j0W>5jeMR`UmU_sDn=1yM&cpGf3YK|OD zFPZj?M1F7{0?Sr+WmnP|r*?nk-I`SZOce<)^q@x%0Z3Un`tb!C2ve^dhx=ow!7VCf z5KS98DjE|JSOb+ctNnSo{ix9obI)U<&7;FK<(YX*auPJKlFyu8%UXHl#`GbV$84rs z{;nD^ks5Q-`B>j9T=tFU$~YD|HXp7(~=r|#(~weYwBv_bObtfa96|B3dz)eB(%~w9s3TRj*q}h zlgBup$RV%Lzek3XUs6u{bqAqoSUZqT@e5A?o4RKJu`E4<|Kc?9gy{f z)EIw1!X$)*YMw{dOOwyE2PSh)^`*YFD328X3U!qeEvHVjH1=*})0*6uP~Epin>U8> z&T{W)t4<#}twxx`=Y@wzs&=;@NmurgguhF6)UhpI-w+vKXpu@p zXsRxZ{_Q%bQg;BTPzsHWWeIepM_5D^FftE94A5CX!o>htHHIQ$+?gW%!^m*!-{aMc z@0H+dqf8>9oJhGmMHmIcUBG3tbl7`R2k={h)Q5f@1c^{36rVSUWa1TPx{2O|aC_DS z-~P&FQPm$J$M3M z9sWRhWEvhp_mo9;C2Jw-%!YGv%g9$cfCv>ZWE- zsCjZ1miV2%WRGTv94m>>lLc;nqmR;T0G%(Fc39Mun^F9y5kP3zSRkDju^ns1Q>g&y z(b?0qrYMeEqoXS?lu4V=7;PD05UY?$vUIE5s`WcgG{@;Jr3)&Tqt*8G^EblQRlq&u zz;)u_W zk^-vNlR6rSi}!c0^9Ms|+-L0)hu>}R&)>;E>IfkXJ-9WXeb(ZtN5$;6hU=u|NHL2{ zhbtYc_*H0lUwp(teR2#=js0ETuVVS>JqanFaqDyL?XLZ-bBYK^8)a(JJ# zi^kikxdTso#kbC&-wP)1HS!b6UmDqy{P=E#n3}OHqnFIZ!gSv+7Fen^p|Ohi$x2-% zy$=Yh@0&4n-$wp!AbSiwEbkF`E`Cn&kx!m6AafIG4-)*%sNiM)zJ->@VHdG+s&v4D zUX8#7 ze0p0_7U~H&KfQkD{J(PY=>)LI{}f3b!HgJ_v8#T0@{_XO2RFm`o)!`S8x=|A@*W`4 zT5}sFthjO0Hj%8GcPSS?Uy0S$v+}I&u=9!Z(nUJB$|W)v*R-}Y3LcA83p!!^u z3xuJ^M@Q*Z{F|bm(}SkfM-Vl{UNUL8nc1U57CNwV(ejY?K-=5xVQ#jHa-hFJpqaaA z1L(~4vqKIgpc$z_FIhTPb63nyIbA2tnr%B@ef4$2zT6CM@*W5=bL!F>{nhuu?%UE% z)^-W?I+RkhhcIXi6RFmkytS%fTP6Md9ijs8nV(4lK5gsG*M9N9ccl-jq(FdHz#(Qg zKSJfYzplGJ&L1uw(0mfkfE|DWCxnD(X%V>z>4~S*>L`|McWjU>?H`qTIt_R`Q!VCZ z{@A_>z#~Ok4K4k&>$$XO0;xd8U{$hSAQB%EM>}d-9~#3_*mDV=uA#yq`gS2Ok+U4sH~@KFMhI zmA82|jOm&r*PXxMv%>-~C)3Pu51;A1As$s5=*;F*1PuC7>f%msoPF2HNb!37vq|gj zvbZW{BS-uu?+TblX$A}oamV(k~6h22^~PDpPy13VQmEHAc} z#nKMD!=$XKJI3bY@E!k!+#Bxac?pL5xDB#jpBRC4);|w=oaP#-#Lu{F6@Kan0h~(G z){4*w9e*3x73?J@N^u~H$Zw>pm~o^s6{GFNxXC@BCxFqq13kPQ(XHRInKkfwexoqz zB_nuP%|Ba0_YK#L#~Zu}jzDw~^C9(=;69(WZ2Zyo)cq?%qf=kk9BluqQ`LQ~&XXSB zgLZ1J*tsZ-nqTPnBC`B>7OCVoHd5x8c(Cv0K=9E9tuj*hWUnr&m-$HF;=ZR7&^=v{ zt|-I4@*@9dEj2l9mbYa9J#CeI=F|vq``|ngZr{(dZ_YQZQ$z_+78gdZzaf?ZAl`E^ zrqxWED({qk)q#OO8UQ8lq2L(163W%1=rsyt>7FCwMBabL0o}72k)56i9NEd95DbTg zWtC*+%(6wvjMKSG5x!oBU1jl2PWLvAp^$rw<#37gUL-UsD*p}O6}}6z@!c$rWzL!y zJX4Y`pM1d9Z8z%WZS|BS%Oj%0LpT=58NlD^;S=y5W|+y|KzLyNd=z)y1+oLzl(=W| zr!E0Iyx zDZ;3RRIloVW)How^;tIi@8Jipc;`&rOy=aA^=|7Vm-wr97Q`9Sns7hJ+~gyprw#}Y z)EFfPAFB~ezCIa^A$|`!L*e^|vkaX~fnNtr@jJ6ckRXVS+Vl7J#G*5;oqXYRVqh}9 z^Ia>;jcR?8$Ff+?n#2Qzy%kWu5=vKHY9I336y2|K{=)cC5Ia&N)gYzsV1Q#_xz#`K zfSA&RJHP<{CSc7Oi!4?U=mrpZ_#Ls&z~S^+v>%z09@0o`&QN6yTdH0Ec0e`@6`aMY zf$R(Oc;i{Il|nMZNn+DxCZ#>-Z8gvIJCv4nL83Io|n2XevIaBXJyj)?!37Z!sXllXm6QD6$IXL}_BRmYC#E7;LQ%q2daO zDJm*JzU$u1s_`8=Qq8Nonu3ShrU#!C89lJ5Vo!UV{v(o(U#O3WVa_LVBzhyXGE+8* zGQUEFvmt*Uau+huCuH!uLD+ZS1|W7e!B};m+wyR%evXV2D_02jnIb?YPk<=Kzh03Z zTuY_1Oy8vVw@#G{A;UnISfu{j5JBTppqpO~kEwd)3ffi!NTx_cb%iqiQMYPXlB6=9 z(1tuFrA8tG@`@9_fddcZ1s6Fx#aCnT@+S|8;~<}7$yQjXhA2#Be==)nx82n37}MNnj- z0%-@1lhsf^x&rBvyNd`tkU-ihVW1T5GLy(*!00Ra!9f0_jB%Oi;mAkV$T4xM@#cZG z#5mM*Tp;{QyTPA%vJnBDtmEhAGOJkreK&g zV0_!tLWitkJ+g(?h&_m9KLf0>JaPy+44XqC8@R#p*idhIe)u%@IIP2l(s(Y2i&z&K ztVSj&5RwaxFq%s!AhyPc;9ActPH92?ZIVx7X=wj%gThsjqcFiOT^jS?E#Y?6@ZvPb|n>Cb^k+fZyI+3n~A$J`)F^meXq_L3>U7`Ep=n_VV zUBg3JJG+oW9#}i3D?$lk0hVvFl-f#KkVo?1ZNWXzcTu6h!i2ahlMS#^UoQHsKEp_V zpL&U-B-X2%m|$<+t!{l3w#gRMNp2jq!>s<4N?qp>TwQa4+J=%o#%v)m<3L%>jeNAb}Kx! z^)&D~xX3K!dx1Vp@Lc>uv0)^Sfieeit+AQjDT+<6fI4VN8*aD;AV$PP=k+)lM=PNY z!L#4$lGB$+es%F z-{T4tsyy{rURP8VZv?TGOkH134|N8riGQk33SLw4EAR}?|L^2-VXkXv`)RB$H16Lt z?I>Y)G@fn1y&u`0<-bQ{~29L5(f<6)M00Mt&bJ9HU%2EgM#iK1A8#TE7R6yC-{~Lz-FB@z)R%5Sa9J-S|s#PdbiqPrjZm4A{iwejnikv7Qnu zO5is<v(xk2Q7N(RzNRNry^rw@RwDix1`k|#25%o7t%f7CS3!~y7l65w9bx#B<@A8Qr}pS^tL=CL})OZFL= zFMXN=+-t-$k^}?}toG`M=+iFU`ZBt}2tH*(QSkSA$`LGcZ+>`vHrePA6S&|CoD!lg z`^(#D?R0U`;pugF?!~`uEb6GixULtxX}X-Y>Xf7tlRR#nX{c|k`S)z`hfJ?Yv-lBx zNqn=KwM3|W>yj>MvF4xyOF7B2y9;@{G$kmYr#>t?y#5rOLM3!>PXUrwa9r%C=|`eQ z*Q!WXP%~^-8VLVzJlw;- zCr8B@;$1~XkWeXo#y`Zxf~Ai$E_Up`^z+;)nQ!O}h&Ta&MBs$1AnwbIPz}JEQ0_QG zR%ZxnEIRsi^t;J2wuXgZhAIRjcD=9fh_E?5NC0xvd$MYR0wT@7Q)E{^VT?CgM4$^u z9$2~vJyltYEMqdt5y_S1=Bts`&G9+)xGA$i;4wTIDPJb|oNhuxu?9ZHY;7=nw$!{F z?yL}Yqp`4~q#XWPPh3DbpNkv_>72G-}i46!0Vq0_5;vNp@`R=bd5)QIB@jK=jyfytq1$u>>=-Mnu(zS*wx5=h+80WDfb0x`i6n}?ygG?bD?%uSfPP`qX1 zJf|S<_&qPe**`ZVXGDd^>XLac{R;gZ?eL*AS+fZBRyqMhh;75mLmnUbt~0ITGo1>%i~@QBW*biIHpTQp zJR-X^vvVvaV@ICY9|y(Mab`B-ai&E zvUzaf^Usz3MgRUUfIdy`#O;$_djF_95%^Wmm@jCwwwQie1T<`+@Quc8i`M=c)sIei za2&L^s`)QP92xs}&l43bWED&0ax3wd52n^BRZK}?JqmxdOBFf|8NiQide(KSOJmkY zCo5W7IEE&pQ+Q=zuaLIR*b0Be5qMc+P3!DHjJGwM;Y9fsOj1uFo$N zdzpziS{r9!_2GX3}C)(cVlGT|+f@6S)C>~kFr5${Mu z$Di2PkV2Z;g{B%bHhS~=ZXPA^}MHx*RBp!xB59&yrZj%Iiz$!Xu zjOOLGql^4f+|yRwAh|y)vylibs02E?F#gbAl&o-SLe=X(5d-oHNnk`U@VXA}nuwu7 zjlY-Sn%qtmu!|}xthnFs*E>24)C@#I>4_ged>SB6FdqT#P1c7qY61o24I!J5)3QG7 z9@R|&2mUo{eJ8B~u)GlXS{dblS+X=8G?-tAX==dWbpCAB{!_BDEIV0fca35jiG&eK z3#4-x)2o>$<-Ak%)W-7Dk3d-0oqii=B)L-31a3+_wVp7&MrwH*IrsUt7+HHck5}O+7y|d*V!lR=dL+ z3i<Vs_8S0&@Bbx)`@9+lPcu)oi6hQiw51796k-Se5f8r zMV-LU$fU>=$5|>`C8fwy0moE$Cp!sa*RZ)_pN73>qMtP3F3P@yU;xM`hncjU(-u7y z01aNnr4b$zq0wQ;tV|5>3KRte8P_oJV0*6CcCfgpR4AlSaiujJT)$MjP*{JM&B2YW zJ->KzBiehaPaoEUcdNvj0XvaTgkrJZ{=0#C$o&?4U`pz!PjO|5W8SUf7f2d}NWZU# z><{w5nFlDzPIyP;P8Shbeyoq?6idWWG2ym(j}m(t-#Ova3=cU1q3;B&*Sk!%ETT4D zW`NS{eq@m+kq)$`8up#`aKT|PjX3D7tXzufcCgAG#W3>n4Mul_#5re>mdJii)P|l5pWnAczKz+9b!rO0FoxnzD;3YgWLP&e)sRKi15=jxLW}WzX@A_%2T%0slOyT2awua0i=Km;CNKwDz$k(jZ z728MXqX0j}eety!OG?qZc|ydHIxmhG`DbUz41xmQr((f6$K@6Q@TlyT=41fe&FbNR4O|_EqczqBVU$WxES593#rWt-q=P7UcrIpL~=FJPtZMs%~Jo{a6j+mqWDk zah1fMvmBx3hKoZ^K0O-idxqj0l}B`N^(vH{$mDrtZOypHIpv{^$Fbvij%JfvZdUnY zawZSv4s;8}fCZHVaOkz_?J| zNzxMKo9)l5Q+_n)gDsjsSTn(w#Ux;4*G^{H5N=S4iN$v>z)rC-fi8WO7KXNTfAq6?E+C zUsZLH;GaLiCs7I^vNWaRwjw0d5KZ}}GaSgLgd0y`U=lX978RL3ka>!(qWM){GIg_E z&Qfa-ry8aZWuG3b@S4)8#$#Dq2V=)4texQ>Fthl+aN9 zBvvSdnYtd&4tB}TS9pnh<;jVh2UI((8?PPgUE`c83LpF?JoT;O8r|*u=Re*nk&>;! ze$3EtyJQb$-HS)@hA+J9pZ26)un08RNKD8DgrjfJOc(moQXA#kRGhrO+khNIe;Pi0 z3Oa@zZ@H5kH#AM3K7nNiV@6%r5j?tCE6H%oxJ0!Gsv}39?{_L>n4LaQ;|c6ON7ac1 zY*by``WWE}tRS_~y#Qz@+$RJ0#bD#7Ct`E@hK*uR03T-@|F#)nDF7HHXQ|qeLau3G zZa-mEOR5MmDAGCLGl`*a7b)bk+TJ=wD>{U6>GQsK{D#{36s_&R7N0pQsh{J{VSJcp z@2=3v{`K_~&1||_q}YBoCVNNLF^7(&|4QpKY+=McDF{6%%X2-SwSh(%^2lvjRU?pU zU)akUaDb@ri28_%irYa+n*-~Vp1Om^VC&H97W~>a!+1|8);>y9qx)9u#|ztA0xzJd zA`P7fi-`94PP5al^m1x$M%2z$gk++_?Wq5Ysy~m1s{jAT@k*sd>7|Cc#qA%aBzJuXlG!43ts-3iEhxUJM3ol?jVrF0$h*(i74dL&K4ye6)Z9gngRfG?L|IYE_ zZ{W#~4+O7^G!U=e7>6J83kFySk93s(`S=6_E^jK80YIJF754R8of2?b#{U&6ALu;g zcwtx&5L^vg056I6@f9=vwY=Ji^ZD?A?aNgR5{bb79-&mO{vo;xaytB9A+(juKVe@q zGI-HFo!WA`tH9_WO}%Pq6J6XL-=w1;a#dGFp#W^sS;?B|t%+Q!8e|f^_6}E?Ck&zbAqzg0rk2zfg2&F@-XN~B!3${Xa zr){_{hP@!k*tD%`_cDn2`ooB`CJb`9YyE53pU3O>o&&r`6aVj({XyohpQ`axbka8c zfb@~H;$DBvH>qlRU8?+g^W$Oud=7WMK1B2X2X+K+%1@ za{aF5az^~|rO&5kfsC4i_CtgUU59AA6EBT5S9yx!K4fvmJJL|M-nTEM2w&Vr5sx&M zq#16dJ#1FkL}F^Uq2vOK52!!j*kURw7bwOf&PU1)0c zSXmG!`pOb}&`%}ID}WrCOcD?Gh((P%T{hu*9lLB*Yq|UT0k=ah{VbY{k9#SuvF#~r zmF6=cda+;WCsiW@CucKgvr6!eX->1%YYd*$Z7qWl66u*2u-!AfmYLp)_QO&oBp(T} zyH>4?zE;9ZhuT)z8s^Z@MwjiS<6;Oy;n(*RcoY%8zc2vZd22t;apZA*TC;VP`C&Rv z?)j}^7EH&VA|ASOIK3|Y*$)d9;0(oIoeNW_#J*ry;-e0EXoI!nd{71AICK3hxfXDP zY{~WKa0^Fb{=-M6QJ!C5%`*SP@r8ah?uExhZNCW8i^3 z^8rr|`mHX-yGF*^qF`u+cL_D5`mv^dqt_d>A}L@_du=x0)@+_bO-eD6ybONf0lZ0W(>vaWNl&{sW?S$##2w%E4`mYFo6f`N3Mpn3zz z7~a-*%g7f|P~wX(dLQ}P`g$3@v_7ZcEL`HWQh|s(lYi@*6@GL1XE2GQ#e9tAkWyOc>i@U5Va#XoBBK7e|GOAAldR^{I>$ zH5jO3_H_%Yq-0pG#b2*JxBdLb$tbb2TeEIjkynkiOEnKyt(h5Y%|#4HJWZ3iZLGqm*ikT=@(07jHJhB@Q)FM8%S%XSGZ1Qm5ku{o*T0>Y5 zkFg%BkA)+P!T`E@({s?_cr=uv@t+FernLOO@ihOcdU>FK1Wfkayr2aWKC4{a%`2P4 zao~Wu5A-c7*>O{;61zoo{b*k@k^PKuz{r#*KX9b|8I zzp0RSz1K?xb((op=y$@0m*%IGE)_>@+V55KI)k-yyH|R8JGbVNg>Rr=_(Nh;^^`4H zK!gu)wiat1#&F3)maLg<51G>8CeYCDQ=+O)KCUVud|8oI{40oa46D`j#RT%wQAYF= zt-Ipt2EJGbo0RG01qI(eElss+lq%k>MP0rcDhted~od{2r zQ!8EO;0TuC#fu#*=^mu_W(xT(JEUau5(TaTgC;Qi;sh_*aJv+pKD^5FV+GFJ^nbiO zFGxEr`SO;aTl8#6hnRt6an03{6X+Sy0CYIj`~}3iJF8i0-w2b$3K*T7fPv%iXynVV zcgYE8dLrZ})l{?Ag;kkJ{+@=(^HKdHsJSz{C>Hgsvg*@R95Gfc)r(gQ?|fQO=L^oC zI>UHF?o`O6_26tmdZgdGfX3)$V}P2E$jyxzELwuGO+o2d!pP`SS>#6H49*%~lzfJn z7t3X(29^;xU>)%6HXDga^KPvk07u;MD$o)eaLaG6uO>~qiVnLgz;&?)$DjIr zcwmL5pAhc_GcSI2UzA$z^s{{dhDaPDp(xA^sBE2^Fx)t+jVBHspu&@ z2FL-it{hQl*qR~!Xfpi3hqk7YBk&)PE+oU{ zt$go1|04x^x;P+Tlx@q~+~D&At}rh1Zv-PAw*gzkkq3d!UGJD*I0X`N`RlTNPaB$u zC{3rA5?8gs5_nwt!%_VaT+i3gy2I;$hIjo8HGIm(`wRmCTiJ@^_K>(MU1_ESH}kbb z8dv9WLr>*DExIA6lC4B-wVp+gePUfJL6Kla*1ktJquCapR@UwcQaGgUcrCq^k32^Q zlV&FtZyC%2!%{QQgffJQf$HL5)Nn$&G;UCe$`59P z8kHIYw7}}QdJTS`hlWXX{!Us|jt`2`fKf~=P%GdJV*$Ev?vseqgm-(jUgaNzLh4s& zXnsexypJd32*0SddqTIAn#F)+y{pbfe7<2LtpVNp#0_Z;H|8xhFV|!nh>{1B@aG$Q zT;?5M{VlS8-)2PNT{zNDpRb3{VH#^&!Id^tL=h&F`I&5KG(piGS_)pH!0+6{aEo<0 zkdbsjn9cC5QA7kgW@u3i=SaP%}{!fT(;Frv|F_6#iXxGGEwz1fmA zLD$&n#YB>?_@$`N${jnk@z1UwM{DiQ>1-*?EVejo)0XxuN8J{E*e~{V5H&{7ZT53M z@6NAw0h!S_3fG{wL&s9jKA##F8nAe5X@I2=qI~q$;3G*cL$|>JaWK4!8?eS!iXa;{ z(Xxm^oD~F*8jt5X;g@`Ps}$~}2*~ikuQezI63F;Xe>~a0hU|gkwL9o->bh3b2efgl z_DY;TC&hVh5M2Jwtx^2H9k+BnwoN6f|FRz1FdO*tc_26kA)>W|-`)};m9Vi`?c z4!*-07$vwH#g$Qq^ggRU4_J~We>aOPY?UJjCPO2>V7)e_iWh^V%HlUAff+2fo5CB1 zxD-~D^7tnFMCx)xkf0l9%1J!~dZHNuW1N5__~+k^}$xS=A&}V0`LL=n46Da1Q7auz3pl z^zPX9c^Z52)oJk0T^DzISQ_KQS8}Kuu6N?Zw;x|zPk3~?V8xyc@gWiMDSn6%-V^%f zFb{^xUW5OGq;m}MFcUk#Bdvvx@ zx!urt?h)e0<$a(ef;QsqD(OYxwXQcAL5iimk;wL zgNJ5@0!$i`*Nf!k;W%&%BaApQ=kW*+2-!%(M}$#k&)0o^6ubK3tE{?`2g5io?-?mY zQ^|ks!?#Y66A~hi&K?@35Wk1S{(_0vHI^=p)MC8no=5>V60dAS`>}I2CM;RZkNn(Z zkoDW=#H{L^0;{i9mRJZGDI{2xR%-vT)+)2NzqWp!{5VpRxsCim+*uOG zlUI#g*_gf7+i4h?Uc7F0=Q5UR*sFF93eO1$FKM-OA9yihD`=Uqx(&A zUaK(3a})8_T>)zL1>tQSxG4v9A=vsvmIVL@0W|nb_;2o|;Gx=nA86S1ejsm&SkBIZ zHj*sy>q&TbdaWP@Z4Rx6A@=Fh*Zac|f#v(&bda3fYmN{>9929PzblFUcjRCDMwLji zzl8f-{+SQLm`OZ+?J+3ZUdY_r9q4)ADl2wIV6tBDEsS3#Y3pZ*FLXSy+uL37H^y1@ z%jKv6X|-Q!47gYb&bELPzH*H2l7bP&$I6o&)hN-}s3(AnlqW-MhJYbV zZ|dWo|1>WBpZt>iNO~=;0XGiqzcl_jKIx`O$B*af4Mg=Vhk!Kc4>z2j-Qo*U?s!6F z6K);0+s^d{ctR!LLZtab&F|o`@d#16Huq>l1^4dwYi~o{1Fn)SR#F`sS zO6c1}2!yTT!!@?r=@jU5HM5@c{Cedzes&agD2BU}5gC#Tbh6msvF@RjG}1u)?AS&M zi%#hSEOip!ot!uVwaUp?Rc5%_Mz~fq3MP|eJ2&+_=)Wke<$f_dwAYfj$s+>vUxYx$ zVzm#klD$?ZQUh8b=@t-y04@`G%Yul2wfL}b=!p&zDN}QdB&nt*O?<~%kx;P5<0Xfa zC<9MJaFih~7~wk4&Br`;tx1w~)eW3CPCQ7JR2yk-^N1x(uLxqf%|5BE61Ua=wb0a( zeFAd=@G%~N@WeB#{f6qu*Bo)qq&bzmc+P6}nwm)oQyN#h<+lUd!hW@*2mCZus015B z;&+HV=9ueM8M4;Sz+;6)s$H&q)1`dL#+q=$iJ;7UocAu7roS`oa;rANlC@_64;>_^ zkKE7)s6^f-$U|_)$a9xlKEl$(U}yYo55 zkasg(4W?r+vlwN3)PjbXD28>Wd?*}GzE{Z!Tlu@2CqT}*%&$y6yk4)^3uRX~MM&El zjy?MsOEc-`kHw={_UPdSFq@O*C{qFaIVW`XP2w_$VZUE--Eksql4iJ&y&_S(K$2uS&nl1!>pI_&M+zT^|dBVjHjzt1HiQf}ItcbcGF@lG;21hAX zf%L@9iAuI2k;BSS3S8f7i3X|h@RkS%eo!j$upXcX;5p%`XYf=DSQ`lW=?JaV&G^dG zOcPmA)wk!gFX%mbP0+~>G+-pw^mU)Z1;&|bzcy(ud>Urtn3n1~8jA|zee465#-!_N z(%}G@q7*$w7Ej}i{cP_bedr0WC=u_T8k_t(W{>ork4tU)g=$5G=4L*`HmT@zS>bC= z&>~A1`V#X^O)&RnCy(>16{+2e(^=9ewno98k37GT4du>p-=2^GTBjj&v~ z0+U8w%Q}~dG##|;S#39*>`X6`d?xE(88ASlz4SEycWfNVShon14K{nem) zny-*U!=axsynfRDEgmluC*C^4?zj&!UqpM+|D|fYxR9_;&l{>>`a6GwF)O^mSvY`D zJIJFPZ!fC_5bNQfYFIxy%_mE8XjzqC|C7f42aKTjFL{V}25;#NjlqoxVH`=c$#s7$ z12u^$N_s)y?g{{5JX`}zujGTSHP4{VO+win(kwdX&l6dfab#OWs#~KB(1fTVrpmcA zWHIvbvke+MIDpo~sqOLe0sZNOuuYl3-a!`8x)zoMk4V6Lck(9bUJFBRB&#!Ex znMf)%h#bGIvFNK%BfqqgCbSrsEn6vy*$9nA$>=drEo7ueX1`f5EPpXvX(4BFAoh<~{3?*uw%pXKY9hEDTC5E_U{*r9FjIuQaN{NB@M z_;zI$L5XiEx@WE?)LL|}-*5FJ zA138Eb|RA4ie%!#tUKKTlt&#(2_Hpo_-IS*Q=?1^5{32&Gh98pWIC4_K7$bE_lFt0 zI{0%5PcI8vfL8@~x?dzhLq5|bstO4mZTOQYYbuDkM!=gn(YI(^#R=a2j51g!Q_DnsC+yxQ_%`EL&G*M{ayMiOe^%L|B6^Ar z7P(^CPPfOC=hb3$=aIA|d zzsh4?h^Q(#Z0Wn7F@w#|AFUV&UnTNGDf2jG{2yc=lt+q}E;D<1Y34B?b>~;UlY1Fl zKQxYvG)4wF$e^vN>D@+65TK)XeH3gHB5*-&_k+-fI%Ufe>IP$pT?Q^er421Fqd5VL zqGAlYn-M@INh*4JmETu^?D3`nb#h`&b=-}3ToFRb)&;KIEBQTtqO>yHR`IEksXD`0CS zHx7^%xffZh6FH-?lsx$`T<+KoWa#Wj(0LBduGf*TdsySf*=`knSzV9{j%GU#Ld3%D+hk znpDckiGq7*=x(&$Sb^@*cIe!c&X_8w;WYrO98U%XI&lITT+ z;~)QrjgPbZ^}p~VSl#xF4V;I1-nAePDSQacS2}pZl!1Ftn^GD@e`jwAppfSPZ9ll7B&dF}YL@Xs?M`|E(-VpFkv zuhTq9>&$tAT%0oH04yex|P zQ-_Hv!~xd$kw8U`Z7LTw&X1}n%xDB02n%>y(-~2ypx-wP$vfDI1Cey9Y!lf0?Y=~M z->=BqdI7MXEN~KbG6nv6y_R)6*sZqt(3V?biik^6jw{Bad(LAvGCJei5+X_Eky{ZM z9oRPp1yt}1y`5y~JEG8z(y+WBb=~OLjM<$UuU3bE5#HOr7t9mv8<7GK&UbU%2kJ#` zl0mL32X6IfA@E8VEC^c3K}$oLbdj=x5#*Efb=31tVeh(+m+ID^ThtuN^qU3torh*S zTPlx??|Z!-rYtB~?NCt|sccxW?yR^bM2q2Wy}?9bJ_F_&&y#k-cVOU6C5n)v`OPpg zVQ}IH?*O;APS`rzvLQ=2=RXC&=Sl&o(jVX813Bq!wA&*43g2u74*_--o?x;ZfhD%L zP+Ic`+yVyMX@Bqm#V_$iWc+S{Ovlgbxn_i)R;P8b;BnzUamT}>KgJDR9(w%;Xgo^) zk-?8H?&Dc(!kr?<(q!UF&Am<6_QOsf!>g)E-E`2MV9Hh9_65E#0a^BR5PCWB1acg; z>@=0U+Ppm?bJ%?&anm(zHLVtEXVJb=o1T7vrqFX-3>sXqM6-Lu8UYs`r-mv9{sGpV z;GwY=P)UahNK;AbHAri`gJc6{swq~&I?T%&>GoI%k5dhepKUdSF#Ut{;$bXhc=A9h zEW7nyyfZwLqGXbkA)Wnv#V(1I8Y6U8p13EEI4p z?0z>TPX)(j<$47TXd;+#B(5!d?GCx^F;yqPmyLktv0x6==nJk5)-XzP(Cj9B0KJw& zK}){velIFv@Q_peN>tO&Hs+U8l6zi>_{(*HusHSJk@oh<#E;C&yY(?2hIgjXKf4zF zKR8mnrR?8exq_)CuPWugr?vhht{cJHbAA$cSyQTR%CAmDaycv?Ogkwzd3nCU^Aul6 z07r2Tx2&QwAI^gr|HX_;7!-2EUS;^T*6dFF{yCUV_pAC}h)qGnAMc6^JrCEW+)m>5 z|5D4LO9yLR#EU~=6q7$Cw9RQ|B}s9 ztW4qQy*qZ$FrTfe0)L%v4M^IlBulu~4k+~M1Oy=srHTJUX+;u8?~%)#L+nX3e+{N3 zYunVO&jvYoQnespF*|MFH{{aufRV|J0N$BDjjjN*<&4i`^(!?_R`xwc_cTAnp;#GF z2mBnj-4tp!KCE+C@k{zP(>-rLBCg%?t|@IHB+N(<#Ac1Z?4yR7&0RacYF>j$?=;u* zgR!bIubMTpH-f)5$vn5|QsL_s|K3q={YO|4L0_{#E-N|8(~M2myZTI7Rfpya+#7 zJ%f|wj)w!(pB!{qjjaHrcI9s74|+dJJ(W)hHZ;;~9fv?@xqA18!)-5$A9xXKc1?Nr zwL=eLPrnhrXukjADdDRJE-R$H*|vjv5Z7W{#Lf$B;zN>XqsTw4bnb(&)yW1>y(YuDe6eR$oqxj^`mu5Jjk8K8y8&m z&c=Zb%)}|c;5TX?QT3c){4hF?LV5!o@f&p4QXkews4s5Qm+F$;`6fo*OGtL#eH^|w zf3L9P8k^gKC8H)ZVh?}MS5GaMHw_{GAPpE^|hgH^78(0@&&)%%*eK{u5!k1Uj^EEZofwN2-ng%#cXcv zSK+NT=gRs+8ZL0S;|4SzU1~aVgHMixI3%w(TDlJXsL^FkhG0g$3p1|vMSoQZu{A2% z)vG@#T76tYI3)C@^g_-qsU1%ym@hN-{CSH?emzso$-6D_?4QVg#Gw6`o6~0sltn8a zV6&uTFM`j&IPl@k^w07pqy+x6|uEXIyE`89^YtFag%y?2%6NIquVIU zQ>U-^?67&4E_%)UBP^qm$qk$nS;@cU10o%J>b=_wn@aDgI+P~4f($%&Eo{^FSUPIy zn>*v4toN5G;os}lvR%}5G5g7cPrQLKzeCqF>A$y56p#XNWM;*rh1iisS=0I6#j+Aq z$OUF`M6q326X<*TNO9DKT6#M%K)Q}tWs>wF9bJ6M?*8(ma1wS$#M*q-=wxx|mSj)# z`Fzslxhm3nu~6B^cf5}_2GZwfn|11to{ZxwRhO90+=kyX~;L1F1oa*|nn@$HXIIyv46wH%ff1CmM_JH&>Ae zN!a-fSdz3j_dp{QWp?2AhbQdXD8w6Q&z_xD+>=?+Km`6XkOAC)2H0sko|muAS+#z? zw^Z;r*!j)ph|caPrOvdFzg zOYcN4ax?z_zg@o*g-H)rj$P!!ml`Z`3p>#ndy5UiyG6V}8#00ajY8+t5m*!vzb}m= zlZReupdRcKP&wx8d`zK(74zuiUF*6=6`f~PZMiMWsEo8LmIU8E5njy8Kr(Uw`tVR^ z^M~KccUkOHg6`&+?jRZpz5R`AZd`rQ9M8_LbigZIR|o% z_*e7yP#>7Xz?DGxv@k6onn#7Cn0s(;_GBD;Elcgfg)zuoO0GA>G5+=QsWgDbI+Aq#(AxT+2Or3vh! zo!9y8*4_@b>7Gee+A3r}oqRR6;(%vpee+c|ke(#Ab}avN0|mb=`)I3Yh=UaEMBDfC z@3lwnvnVehBxbI-bgeuTL!EgydaiQ0r-|-I-+VVOP$BgIlXbJ_!Y3WQXUq)Imob^) z6Vpv|Rc+gkFgv#jU>{heyB`fT*mJ`D_eJ2O;Ks_Rp=K@ji}B|+%!SB>BbM@ZT&P`8 zXIt@iV_@ulue#!)Yg^9<$bmo z%*Q82+O646h()us)Pj(2)Moos_aQabmLD&uJjh2$vz1fvEOXd~68?g5_0${T@<6a} zfky5~nG;B5RqCxTi6FVn&|2irPAxxDV+1BbO4E<_gge=gYpF#vIBMb;YPR#6xFG(Z z;O0SZl55$18!}-8edVfC{SZrOvtSK)8`Y&O;K1}oz2tNPgLuFs zLF22m{}@-zqcNP(kLQe5rut65LyEFoe_ydA5+bHrLVwD-ar>l51qNTF#Dm$L2R0Xm z{d(!Y)fIc*93T;&Saoe}w1;YI@X4%fjaMu!5o`@Ub#(DX*YN~oK&@P6J9m6QPq(#^ z=8>Av)r4xkT$9v&`IPq2u>JerAa=PUf8*M=EymM(&u!|TK>he}oii14>aOn`KBv?S z0m>Y$j0tm0#B;jIBElgeM`E>IZF)~p;A*lU*>oYYM}%=& zqP$)iVntfsL`B|srK53W;P@wRN~5BkZ~2Zcjs4*8`4TK*D!rC#aM))qmUOBZ4PQ2G zo$EUlbzsCi?B?5i9Y*_8Yx+>6_vT2q_b9fsM0+6ppA1_gS^w+=$bTN1U{(eMAg1&a zO5JWHv8eAbi`?s&k?sSd>6*o^4x^xnlQm+zTP<&5LTv(Hjd^jY&Q*-6&Ksu_XDdn2 zqc%KKr0EtAIon(233tMYS9IZ^UZ5?j5gJmyLsB|v#-=pb0#0yyQKoo%38(1bgeF85 zR&t$*aCM4A?_o4|VUghi+_Fnx?>OeSmGfnF8LHEX>(2p1G&F;>jBVB^#+(u2zTCi8nY2UYXe-%W{(dD))odU@bpL&}!lrsF94m8jSIMaGe~wE#V*CZIazj#Hz9%y89-HTD5i z04oO(G}Fy!??pABt0s5q-Lb!$VV1EJYZYv9>X?MpR5+{!y6K45^SR^S`&D%$_g&Xt z7t3xihuxo+mOgBH%3=SM95F??I{UZ`1(-2MHxQ*p?dINWlPwFK<<0n~{-lQnRX%F50%&T^G9fyqbk30R(o|9MO%@$34TVb6zMm znjigWv_Kpm0ZKsss+rdI5gUfsfm7{+#psW_z1z+$6RqH!wnANa5#_3tfJWQ0l`BNe z`73Jt<>*EHuNB> z^S0LVpi6%z5jg{-n@g|Rv{%xljNat-8t*+n0JVB=S*HztcwC0$#?bONC=Wg6O4G1< z72^ODNUUbfxf1^w;AQ7~M*lg{GU;d>PDqvh+`Jh$)GRsR^W~uL{&0bPPeotWwiPdn z8QoJ$?fY2D67Z1jd94S~TB8e#zc}elUhd_onYy(TmF)vTm&)Iw%?HAJmDKl6$BgnM|QA`MfXQvE^LI)`u1d@^$>D{y*-{6`fdY6H+|t{Uu$u-RZSIW8_yR zUm-Q-h(6O75NjqME_l9G@R`7zKpiED%(=2Be?fC7>zAS?n@STfTZ;4T>o!!nU+Imp zE_12gm38E0V3yVKLw;pNPUpnCP2At2&Y#}2radY1nCo~(%QB6;l@PimwAEoW`*L$< zmisnbNm5zqQmV~h&G$CX%`l{pncH=IlkuhG>qHOsOzBK`%SyTIM(e}8H{I3;b4S&d zqm802x7>W{lseS?iC9$<2x<$ZU45?oa1NwI;FtdL$t{Yq{XnjHcq56^=!hvAQNLVp zO-8!k&LUa&M&I{$X^u@oa(ydU%2QOJhp|6RX z7olgMpDk^d#EhPIea#(uiXFKt!@FJ22#Wwc7bs`MqoFXK1O=_@z{-6lRk%cX#&jneIzqp^JO zoMl;I&6%zpNOt!>rH-`B`~!c-yu6|y|L`Aqef#F=gDvlaw}zfjVEv8?E6yoSTnuEx>2l8Y!^DT2%a5pKx$-G3R% zW@n@?cK9##dDq`ly-;DZ1<3GMF#r!MsfgUWh2ADJuH2K#Y@K8D5nXSYQ~Fur#?Zz9;tpmZQa2B8?IGE)5vUK$Vj!KJyy={R-}XQpuYckCP665D`YS*b{O_K=;BzBY z?VUzE@b}PKY&K6YWZJ<4I)t2=3xPAID;-5|sJd5VoN)KaT%CV{Dg*y9nw(;VZ8|w- zWr7_?^m&y^TvL?Jw~@OXA9lFo%IgD>t^_?=`#(xS>ukAD`h8pGclW8xtBQ}3#osw9 z+S}&XrI`u@{<=BX{2dnVwSIT{+;{A;VAur(ineJ53?#9(Zyo=xzeTt@h{L&=@LNGW zHCl9DL~N@SkIiFPU{yBJExT4>*BPolpHiOl+)Y*;oV;cF>_T$n211Hu>oxGo-T$}y zmz&6sx5>pGKrB6a+qXt@i|pj5R$^WMIf zIraNiv}Cw_HHP>f*}6PTb)torjh!o(rgZq$DGrUBhVE{xx~I~BoHeHoIuF+|3)bV= zSk`?S>a!DK5N;%Mch{M*)o0C@PvBAyq2MS)YWj4y&(!M(aIuE$7R zR4eG0y^P3vVikJ}4C%ifWbi`NIvH<~nf1EhYBDn0wpul{Ywq|*&~p8Pm$8;+#eq31 z0pS}nT%sjthPWkszx5A5jcr+(Rf0lG9Z8Lb1=WmaQksCiSdIN%)Q?qkt3m4XbDZMg z!fyKV_T3hw=r^C--?ki(+H7mbJ)abI5jhW|j?8TyCMmY-7cjJw-qyAT6SclPo%5uM znujg~rk+J7{oCuH5D_(jUT@Mu`9ofq>4@9N10DaZl5bv?yUohpJ+Gs)%|26qm|48A zjY6XgrnSerZ-2jan~bNn<(1zdRlW6n>QK;UZ-%3`i0z1ho2H-tNdZ|3SOIM-apf{x zfk`vdW_=2$R%EnvrHZGfzZsLdt77-74_D*`+=WnXTaUgNX?I*9N@-i-EO5u2FaND^ z`s7U6HLFX7*M(kuY^9o6?GH9JYa`@BgMv7hGwRNVeJR{9@!@CxG$a1*FuMYwtTn#! zDmQOpnxykladmH=D|>UE3Y4e)y~y0h+H;P(`^uH>R;W17&$F0^Mh}g-09zbx4jTR0 zaMyvj&XsRZ_>8Mcs~qfDkTY>M!~;)~zA-W$Pz_X zmB}TE-Cy5}X@s-BoS$Kq2J%)W{8uBYHi0G?f9E8hp{igw4a&>7a&yAJF&uwA=2TNw zsxUI(zP9sfl^SNGkI~nQDr-UjHaOr#u`Xv>HRT9FbT4Gd{c~otc%gSEr-QRvS%4wd zR4Zz@T(_VE-7yLugDe0HEp-8R8W;OqTW0-Na7knKN!fBJYR`V30UeHue zKBrlx*q7D6aFzi@pyOo8(l2BkjO2$xrRJgQ(_$*PV~I;b!XqL&YnQBhTck#EpSfe@ z2X$#_$&)(D<)Y7ck~&Zi6#jMuP*#1TdctAf76N3nv|NtFJ_9rAb0H@vktFM2@pMF7 z>67sSJU~49yN^GGt{~wX+B)$|+#Lu0S*8pne&S*F7BUb>X|k)PPZ-)_fpV-;<(Kn% z-1wOKHj7;x)Ves2l48GlRbO%b4FsFLt{*Rig!Z@_J5`(s%luNO8SASV{Tw~r9mfqF zUdLT_^I)x^N+D6vFBR%p-%V z1l(xPt-ov)3KG;!7``lb_Ep#1b3-X>`Lz@Kq#oWq-*;?f>lMxTgld<+3g-556rZj>9E$W-8$upyGsN|^|dsBtz4Q}-1|P%xwYT) z6oC@Jzq8Mqu@G}@uYBIp_U~tB1pcx1Z8g-*(7yhJ^LxwD@`UcpC71CDkfP))x;0zp zedUicpt)Sa%XBbD*wh`u-d9QM(6oD2Dw~9yK>@$B?yo5yYxuOUIJ)@9EszlCVC?^x z^`7Jt=uzP>FkfgDlL3%FwLm{vArynbIfa%r*>U%Jm)7 zb&rMhp2@E&!i<-CwX@oyPg0!r9+nbYNmExJ^VNsPn@D9YR&y@FJMyvK?arLZ{cIkv zvEDS&Z;8$;Se?hFom5RVT(cDWZLl19J(hPSxDTv^k5xeM^H$!=Pisez_aXDeqTeN8 z+&77N;DRmu!11}t6$$sliyM%Tr}F|jVqf0LdC@Q{;Can! zqi4K4B7PX|Ja03yB65rHz%~Y=(7E~&Cq7OQA)~F-Gqz7QW(hqVZVT0CdCa%-EDa64 znkf_Dz-RWh<$~SMm$eq+&%8gcScoh4q_-h1|&usx$qR;?ZLGF8bf*n;TQeBiqSgrvSn5}wUhO+|W6O?lsH ziuXIP`7mD~yhslq?a_HExDX=qVB^uw?e;RBeJQ0|f;COGmvfs#)3zk|}c*8T0?UCO>AuF68vh7nCzgg?XoJbSdes`CZTR?7bxZkZ_&8?KMi2pE5)I zp6U-RiNhoJTSOS2@7K9D^Qh^7MLtZZMsld8d%-VPxz)xP3oU8sKTh21Lq$47IZ549&=XK~WreYvWB>8$ymrjK=5eGqoI{#atN zPPhCJ45?)Rt|v2GPFTryiZ}nougr`S#{PpH?2a@pft}=JpSu1KatzYsP|&FjL|(~c zqxnXfJnQ;b1=NwORz)PgI1uTSStG3~AafyQN8q~r*XL6r_{AUC@i-@gHN5yT+2>#4mYCp; zLpdsjwjAS}_Dto3*pVRdFKtVM($iZoxJH@%smnarMz4(_?YAaf;*SR>XKrQOc~y9r z9Nh4OM&P0X9t|o*v(8Pc={aZz;1?TA1U(|3f61xXX}-BhH&;RTsCx*$xX`lqC<+(TH`zIz)v`n*x4;3X0<5y^C7oB zs?4UtYFfcrSj955rN*oil*feY0rK@(ell<#D*5!3w)K{i07GT?fb(Q z=jA)+{v2O*E>u{j(DBF*lfE96p6eAdm-Kr9HZPc)Nsu{rwRNJkkvINu~XJ{B#(yZ zE>;SDdbqSEvDe99%d1;H6OIFKtf=;!d;En%zi+2BuuDrn7 z4P_Y-=rn8Ci)RsGB+;RzY7+b`#dhXu(ndlXSiMj5hK6mtVi_-gV@~?yk~OPxrZ@B| zh&AYnEOwppkX6)ILD!_nZ5(44ZSe2St9rAaRKz&s%$PXP{b#2t_*qY*GRaMkw~BLU zo@!zpc9VzXXgIU98>^YzUvG8YIhVaqSTdgZ;6>~~a$M}HC*pujBv(cBEoNWB#Ti=h zH|O95p9e0a>eeCtj%mXjW`JZE^~3kpD?uuTXBM4)KLe7%&f0~d{1h#B8;3sqy5IA- z4EF*YD!81_$|A4+9D909(`_74jqE4g!M7aq)d7}f55!d901OWmyBZ(|m&?TOa!A|O zeL@rUvgHGe8XhFnR=78tVi&*MxBs!?z3qnWiqTKfVsd>ROQPaETp8DB$t@{@VOB(WbD@|kgtu}$0g`j$s=cI#9>_VZeu}ZQ3 zdm%mNp7o)RtioH#gBDcJ<_iTu+!-VZTZ*~ zUtn-hsY;Q`D@@^cUO3mGC>D7-02OLK^Zh=5Dvy?I6Rhl)sO+wY&J31@TrZz>{aRCNNIH} z;l@eY8-q$t*sZwc2hG^3P7_%wt`w73(?Hs7rh-wRwU|--VhPKa{E?5f!~M1Px+K@< zw4~UMkp%Hk+Bm5B6%r%^7A)$rqNJPA8cMB~R@_9(vmd&U&BG~Lq%L>w@?%4qz#fP3 z>)T&=y*hl%@VFzK8ks^zH0F^OPZ*J0OKpJdbaZQ3l*`Xzc}7inZ_e9t@*X#-QuE`6 zoYVZRSPMChb)G}Aa5nzY*I|l}bk-rXU1z!5%W4#_r~H0jz$_civsG5#5NY+ID?!(~ z#o!6{Y@H8F=-Wq6vZe3@{AmeYe0``b=?UX$@_pE(5A4jVlRvW(hbq}WT`*}r44Vqs zZN^ttzO);B`S;;H2hl3IG_7Q2Q?l`HB!sl*-M!7>zCPeDH8l~Q-=XW+#kaKY zxTRX5qff)PWnWWQtUuC>5b2jLF}3|ya2@G4nRwhYaN2sdgQL-8iKb{n@AHBB0BH)@ zYr9~mMMCKtT$O;U2lOb;v)C%}!jm>o*nxDj z`*LuT1C>6(`QDFCtx_r#?RU>S&o)XMP+)|12OL$Ri>b+g&F2Le!O6r(SV63Z+M1sy z?YjZ-wr6+Ych6v3e9K`y+0_X&W@y-r6a_$Ain|x*fq}?WIe_F3)3_sapjz%bIA1l~ z8u^=25NfdHBCg~`;#dPm5@RfPiV7fxFtT1{b;b{8#OLWo>j`O^k41kP9cxa(vP7-n zKYu=;|E$_iI_s-}9J$ht)WAblJ>p*HJkBL)_R%Sv5^rPo9GhoeHIeNd}0i0|!dUD#+whS=2ELPpEv5s%<9W`|ngDL@w*}V3yy=vish`j10 z7qr5fi}9$JYmh~E`29{NVJF^>$Kf;Rs_lG8ae}SsnKdbmWzmVF5h&4W+ET#WPxb+{ z_{JBhd2|Yi-N%XyOb0=shbZygo@-ZRSZK*PgF`w7yJ};^ls218`^e#41Ts^az_5WK zTP64=j-0Cj+C?nPn;|nBTid7|u&+Ut(1}Yzg#>A27U45~Xfw(8&v$lOcKiA&YjuC- ztp7;0TpFFrcK#XeAv8aa@LiR1>XgC7dRO@HT;hu4P@vr0Amc`;naa5>77I9gKiYQq zI#pd^=Dw>Iu!3;9D*HR3v^zEz2=D&{mjUGyOUo-%`HnuvW9FN$AlCwVlds-Ald%{r z0v>#z7Lo0*KmLxv5P<4eqhELLT8?surN=Owc3Bnk01)1>65h|6G8SdV%I-4MP1*2T z2GZ!%XQtmCnqh5enwLQU=H zt!pHR5+p%@6P^|LKkCSuW$b|EwiDRx8k=vqOz~O6Ur$>0T~@%qsCS$~toQ#9B$Fs1 zKcYv?OmksPzCroLTzU$^zPCHzAV|w#)7%_gT!?tiMfTxulUV;F7j*RMt3)Q4!;cC zk&n#_tRDW`LW1M77oI^}c=nj&hRTUF50kfj@6s#2O?*6#n{*zA50GMCDP_W?EM#oX zuoB${vJIJ~pBpk0;nnd-q#~Be4Iz7O*sRQ0Tb{ZFwx+!v7E>tP`RPkkqJdU1YiWl}%OMHTF;fm%MQT zDbL&|Z6i-p8E4bQ;$+?1pK#l$gt7y&<4kPrcH%X#JmT!LE1iD*onB^R!-i{%wHvJ2 zG;(PaT{OduPZ&Ycii!Jz-`L{A9+GE?7Kfa(HnM%|i<}+v3GT!#bVhijR%b@)NXtY1 za9~y_gXih2t^0$B=Kd1&-yb)iT}{lD?+RD0UuVDY@IurB9Uc_9h9u7zh&&twkvbA6 zoaSBaN!{VRi7Av2fwAD1L8Ow^<74dTjoss=Qt0Cgyym6)C)RV~UA!JoM6)QN67@5U1d5A=@7vDHk?|-$zX>JEd};Y&}jKJf6Y|6lR#Wmg?e*m0pvTya}zqPn`TArKe z@xygsP{mWA+WoM0%Y^1e7`4mwxz@V68qy;d^YtIT>a^_sebafvZ&s3=e10>V8bRCN1p_WWH~%AS>i`SoGU5vi1nz#afCTmz91Tl0S=Z^G zCp=%*A^&>s2Tj-Bf9~|L@YMRO)&GD^{oZnxW2Jq0S4vG?oOrw3 z^a?fu)P5?7AZqI)#_d#^=WiS^GqZ3li2QVMnr7ze3-fzxgk>u}{L5^WpyrXT8C!^t zdO)$qO6#7bynBDlIKjn;&)JA?-DF*+?@f0+2PQSi{Bi;ohq68VGboH0pR|kc+DRqU zDiAgZ46>DWKC^BCX+eY&6<$PcX&I|lfH5M8X*&P*Z!{EeX~EQcqjY5%PQlX@j~94> zxqJ&bQM+%n|Ix~yp`Me;(fZ#n5lzs@)TEU$`}aAjw0ak6NSZ(@MQ__MQjml0(_xYZ zML0hV6~EdIo2;Er@P~J3HE+^=P9yz3?X8oKyx27Cdx!WwfN7<=VF9KMr2l`*@^t?% z%(&?7@uw#AY9MnG`EAykd)s_tU1P%no7Z3J+?v2$rgsNIEBTUnGPC;NKpt?jErnIj zePy<6oy}_ro<0~QCFvp5%{NT)UTG?`LMjMS+*jF^HBBtl48nuyR6BI^oFx>zp{@Hv z3yoSlDGI1Z2IFc4wJNOyQ zF4zwY%Z*DhD8@C^XcfQWNnf!yTzA_9h?R3 z@;Dy%LPP~r5(~c>P!XY5PbMHACrWhm)C312t}ESFp1*F$+1k?k3N^3R?Xu&wE_@5@#*)@d4 zuEzCopSL_tkW&PL;nf)0Y+n=C!~gCR4wcSqL{?L=0^0WT;FclcqC1*k$0|I=EWq^a zhXii&v>azUVQ{8-`2jMWG?83(M04Zk<&;b6dhd_t`>#77WmuQ@Ckd3TAe{W?j`z4BF?#`{= z97%0E!S%ec{Nv;CWgwl%a`QLCl%0)K)3F=zGwwe$5Cd0pin#ch2zM~-ru0TcTqbj_ zBgHo$)kNw@KYKyp)=~t|U-~FMS}$sx1YN!vkD3O4tv|TN(NCJ^!<%^3a-&HSJQ?Uji45 zKx3lJ-qq{TBb;oZtwDc!xfod&b0;sW>qjCv|CuQe?mHYd;E-`43 zA;<=uXfY*9*2jQHn+m)^*N(z}lx&hvK*b}D6)X`aZ?vcI3F?oh5o@5!w9kSZg?=ne z#q(YFmJH)d-8fQwV#O2E;t|PW!aPnBMbp_8DK6R16Hmb-j*@})i#^&r#F^x;fpD`Q z_TWJX((kE;q#n{~@zA~l;eNq-e5o)Y^0quRW1ipAAsxf?>)?{t-0QO?P-$J;f%gcb z7yJ<)Y<2T*KN3PMdX2vQYD|cEpJ?Bs8_v+!{4n-O{H@Nlg7OsgaWtP@m#1?RWi92E z@H(4;by7jhTit@q$uf{Q({p&WLF-hP>&2ovB=s5 zM2%lnzcSrWH<6T7e~C2EZFf6i;VQ7LWi%YzEG9a@3d|Ka-9;Gj^$2~pGwF|Ue|n&s z!{a~BQsPQI@gZN_xz>bl;WMe9w&nAtwfOkI(QGKWSvx%ZWhaPul=cB0_p;_q#lo_0dZ<1 zs9yQ3m!G_WAgq@+PMX*ymERBQ_WVoDfE5PwzUuFL^`}+pjXMmi_Quo*iM6fYls6vk zYlt{ds%rA~Ga+`|`tbCY<}ga{H<$bZi@b5Qx2}SF79=1s$*fS;{rD8F=du#GUJw;V zd5HE-EF6awG}u?Bv97s__o+;h6L$g9ZiAM)<`P!5Hj=ycg^Qc}+BKWh% z`p7?QEJd8f~3XcU(3HZpId%8k`>aO;#-99A!LWz01!%1@wqVb8zUgjt z&MWNXH3<*$)NY5Ny2))cb-9p4f6@3GV*Psx{7%B0Bqz@p5P5f0`&qAQ(*^-=s5Y;@r2qVv_N3Xxn z7&%?(LqdYh1G;WV7x2uwDtVr`U0;ZK7?ZzIS?xXkNbC!N*MCs>BGN1~M0mYlij@A;w(%@RI*SV4Jf~0pQbs&Dnk?!C3zowd z9+t}|Kf{Tn>sCF6T^+j#lDIRy#OmCxv;^33; z&?10Gd2wIV3$1DD3TMI9F#u`9ZOSMsf*(Xi! z*YH1Q(%##}Ip1pP1l}z^0iG~R1v?N$@flN3@m19ToDrJk(e$vI1_zz|k z5ZX_F5KCvbgW~!pjtED#v`hk4`toZC2hZlLGm0kaY7OHw>!DUF;Bz6`UQ%Z`egKVy z2I!ZoY8E@LhQJ*`z$o?9M;D?YO4A zzIlIF3^lWa{M-P|qU zKitsah3Fg0ZMIXwi4f3sg!~UokPr0H!9kNo)b*e5!a&nc75UaA=LsFQ3=5ial37cjGbNssnf zqRvj^^09aL{DY~4vNCCx`PZ1TqD0QWe2zv{P9qfo;_FDlPuA%v_x||nIG3qvb(haM z<3f7RpISGcSOzdD0YK*z4;1|PL5pUaa zc$QI{`MN2nOn=Xd8R+$k-$hA#!_%R>2**vj7+97}q z9^{ZYF{cRgL&{}kTy^2t3DbYO>TQzpncW_mur+&>JA@4f8HO0{7h!RlVm2b#({jncTs%mwzPHO>16$9s6lbi5J4ZW{hlN*>E z=CUK?AHW#6$KoD5c{XXkWems&U3s|>Ij!i5? zq6t{TE`vgAum0;--Z&EJr2bRHr}!uwZ=)l2C?20Lnk1dS3v104d-lQuoa%?KDSCS z&-(Sw)e<$dmg0Reh$;GW%WWkKS{m~AJIetN$oWPPO0Ewfk=HKlE^{>`*{YQlSDD2KZg0GD^!h_YEE`iz+Ql!rLXKl%8`DBm*ui|Iv+XH_we znC@gr)e)mulio_p|751V_`|bo+4zFD){-uB*YV`- zEa&s6v}g0nOgWJ&uz5zDT9oU78|w_aNij9|O z-r!bCgu-h+&(&%CTcCiSh@NVA>9fJ~RZl1)QE@8Es6tcy49uvdq=SdbZ(#HDdh zs2OR+Ut15!Jo>RdS@WoAceb^rf;iyiwEQJkRd_$55(GIKe*#j((v#6avP%!$r(|)v z6~M1^92;7bzlM1B%pr4J$xm}Pjih=Jq^~{4&uS!&p%qAb)^y_gi^b>Q%K*uCA?@6} zYxjH=em*tf%_p|`?)PDGk#0u4Qcy|b7Mx;3t)dZV@Y-PA>Or^$2F#c&%-al(+m8N< ztBJ1)K8o~4oB7m>z89)v3OfC9Kj8SQ9n~rfKfTakk1A=HLcpbm(Zq^F9kF`YU{( zs;#-gyiVv8yMdMWIMUGja_Keh!0}oAm*w-_Ds-ZBVu2BFH@SJQe1*0-(3M0*NCjsf zo2UulVWUUuHU9i){`1xK;zqB0I#4piH8ZE+QY3**%3WBK$b>9HZy#VW{#&+e2MIZs z!XbA}zCQ$uJzH7+_*JW~8$Q9O-D4Df%JfD&YV=(X;=1N|Lp=xTuw=Xvkexl9@J93a zXaE!=)JK>ovvNRQ=Z}{K)#0dx#zmB2v3xOs;QMZlmn9qs5xH|*Ha`Y^DkBio^PsX(3k6@1_ew?BSG1Ou$ z+tOU)VX!~%Y%h08sW|@QW|KtEiGi!;cm2MO?-JlXO2TwofpIg_e^d&F@(Efe_n4PC zBVgDxP4+%+p$xt0B^pLB-h4xohfB829fM5%<5KNQYa0rY;n3+$0$wr$<3W$2U?^GA z`~5%x>dKMp)g^YlE2mU#F?lyig<)|qGio5ZNe9K$qx4*65!#%zrLYE-FMulE@Y`i# zM#;h@MP%V)j6_c)@9N4N<3WjRJHtu=g$jmf8)O_L)X{RJF-)JFqOkbJg#9h&4KX5H z)R2Czr{bztp=W9%^2NMhL)rf>@9m~17tIdC>?Cxfg(SBhQ-sHzn#K8HCSGMpua--^;n^U79V4_^A? zgaMkI9S`9ygd#JZlh<51oc5X$ALrL{RGwovjuF@PAuD`@qF=wAvsgxowk|MHpA3)h zJk(#RS(qB*ZKoFYyC9|)ytb+2b|~K;)fi4fhM?j3pe!4#A_z~K;DmFsN<0Pr5Kv`P z;iQwyo8rWbF%jZ!%F`{_Ib>d)n?YRu~shTHAmEx-*|g3XrQ}$Uo-9%@<6Y%147JjPxx~ zfhkl;2!5R%6K*RE;CGS8Akrlm?LiN>mnVMFkB@r~N&&ZXYI~QKU4vi#-;B1gTxQ2mr1*46+U$@ zjMx5wrgGP{aLUL7*vYI1#t=cOJ+Kq`|1V&aLU?qrb6kiVcJlar@PryAW`?cQU zKS*QXh^|b*a>1irW&G%EjSM_ zfD4`QRbP-ynrK+Ug>SEPk__n%|5*^W0u5l^bTgs;U*dJA8tSPMn15Lr(AdBcfvLl1 zE9@=X)dPRxwzoWS7BPQzHVf$}@r@ib?DqX`e+A~eZrL597CWNIctZ)5i$wRmj-|VB zi?vG(r%!Fp=l6=bL6aLOA1(%pg-#4r)S0)4m%JnvUO-ABdS|w8U<~OCz-s!vt27+B z67<`T^P;#lZB~bgbZ0?)W+04qc_Z02S!ZMKj_-O_dn!fqsK6nv%Ss8}(CKLw&$gKj;0T_$3zXuURmtl7i+XAR2gmloZ4z~-LsE}W<#NOqwwwq;NA6YW2-G&>3T z7nO6hZN$l_JO|MInd%!;Z$i=%>n*8%pIXCNRDtvb4WKRy(sg1=sgZQhw~~?7@_P~F z2~S7_k>70=GJ8U!-Q$IwZ-!VEX8=D;y7vslVeErd`q2A{kkuOrytggZAj>~vFzY8n zZw(VO1yl+}gVPGi4HNfvvqY(1P5-9j%Opf>6MK?5eBl>=!F@8x}(e4jm2v*lcDefWgqxQE0`>wA4W3yer`AW0Qb z8$SFeLM#gMq`&07IG3j|uD5|0Z&%1cEsac}bLJ8uOO$=l9Yh=&q;OpvUYNV72ONAZ zo%t=H_$6qM0?9wf{zhuO5=m-4?&6#0Ti(6Wwt?mGN7=#&K{##L+mm2`#!b)dcxu-f zL=7y*w|Jq6CT{bsesMXf!nIoOy;det&|tS}R27@P->oy>yFolTaD09o(k~aNFQ*R5 zIXyG;;IuHle!}rZLwS{XWpfu;e}a5Y(l+Y}8JyJPXE|p_$@9Hh3iGF*Wy8)0X#C0S zjJe;K){m=?Xz5=3at7ebv0T&rGe=X0i4gv+AhdW2KRQemc(Lwrbo(eBRaSmLr|?w) zzg;s!*W+`Ssd*~BozU=hYPNE8<{=_*Q*@$#dKrf{2FJuCHvxgHAlwO_Nem=m1?l>n z|nT0Z^0U0QIV{GWu~1GRptjvxh|!m4hdp7^VyiHMef)0 z-`+r^#lI_T1g#8U*;?|BpM`s`e~x&{T1Of`8z=)U+b*yF7Si_p5!6+z5=kk6^X8&|E3F!YQRC+vphTE1KqSiJ<7RVi|Y$YflS*_$a6_pMZm=gTIZ zZFkXdD|8p-wBpoe4h!h=aLckZysK$Vvwm2gk2phTQd(ycyYZ;3nZ=^Ph)i_fa@wkQ zvuS=AR41h8)PGbKj_2b{s;0Qetis;udB#)3PkE^$;huB44Vs>!M6e|I%^|HK@DSKZ}=tC8A{D2nQ>d5 zcI|cEl}0h3^_j~GrR#~ZocmgsP8qEwys7fuzvi1+O(Ip*vqftkweb z5PyRKGt{(zdHLWCFA|%Er{aShcwjR(r@m~oKln1oht9jZeQI{(G%X?CIOE(=q*ZPYEAc9!B=5~69VXa;qW1uS2QGA2*m5 zeHX5llT@fRn}RS;zdtt{M!WzE9wMck=^;PjK)Mm5_x*bIIYsYCjN#~PEjyB(P+K5n z3iR|zj|5sN;?fKcOZq>j>Tfb_Csmj<)GO|^*37wY`ApjcopBMH-}I%{KXAzzJZfN2 z5u0xSh81^xruF^tB}XGQBT6WhqtDPNg62x^X*!HJaUA>Je9C6HSJvu><5AW<%oYqV z4k%Ya2ZKj2(EY%2I=wLWdJe*Vb^Ue61(#ID*M_o7H9zMJ{85P>i}MSv&UrjChMxL> z0^N?pbxkK6x(dutB=O2r(#jNsYdA-e2q7V4wA;@>FP)Gh zQP+&aM|7|n;%*tC8~sBB552oS{FFl=Vwfa>FW;`JhIl+lBZ{pupA|^od1IYMG&?@g z#^bptMma^(E97kNzWGX&C?m56bbYmkVb{b``Ah%yr@S8l+Up(fB&=1BK3p}PTjjzmBk?NGpU}Zh?!^$Aq`E4zkaLKYvc2C&?C%ryC=*$Lv zkxpLn-)C$%gZ#Uxq85WwOTHJK-hLI5(Qf?&zc*7-+qbFA3l!~d{?n&Z;Sor=IHafYaZ$IxDU7%LUxlK^ zjIGgI$BbZIiRW|bz)3gcY=Mp+hf3VpGm1yIIT!yN#KlF(I!1!vKR(! z@SoKlRu%vP3wfmfCVGd^3ivv>m^cuvuZ*9?sq}t-dMr^Wf=3=hIBDvD_smy{IlI?; z$><7I53ub3Z;HoKjvA9a>Zrb7KUY_&A=u0WnI|FneK(nRY5U*xU5L#{-kgw5&iMU4 zDffYTC;$6hSOi}WY4=~;O0T@Fo=*h7=J2ZjGWyH&-@pdSCZcF7y<%oM(n|pwi!G(7 zfe7nljxx%7>Sc@C{^sEf2R9D>IgjLlGq*#a2XsRN%RGP}kP0u$JAb&m!nU-H$AOf; z$@b0(!Wei0wjWP_O5_n4Fj*jxSWc{xN%*7aI6Do0H}?4mxy_dzEeH5Y4TBqlng-IM zQHe*vPHFUMg7+ZqcAL;oq1(Gp+OM41?ic=Y`19DLep_13om4GutQBrgPx3p~r08Bv zPS9m{#UJU1N^seq{zL2OPt8UX!Xpd}tQMPy=zj6MjzBWL%!Ah^h0snhim1&unS@xi)lJoK$< z2o2xnW$39wuGv($#oL6)S`|rhh$TquDL`(u`)rHC*he7 z=v9siG$j-FL$ld@!JQvL6o2do-sNJl8ek_{={bsG^5U+vmDCz!xo~UDM=IXj;@g`n zzQ*9Ky(vsi7n41LtUe1B4epQX`Vri>7{mz5g zaCvJFEl;@h*9j~TkT*9Z{em$G=N0z=f6=gms{I#OiL_iBI*)grn;Ym2q)2L{QuJBF z`Rwbw(=7IU2&yxp#Q&Po&nzD3dw)T5dZ$Ls`Qwj++E3$q1~!BJj`1rWAJTq}4ICEu zTm78rZi^L%1v`N^LF*ypMdA)`c=0gg+np`kB<@9?%p{J_eGhjsl0}jrqCY^B8V`|I z;1!_iFmVPuhE74O0Slfq;*0;OwA!o|w|0L6kuQc<6EARG^vJ3}{arwY(rN!}{Sk7d zOd^cfcDkb82^uEUX=zx0!GxjE16>~xV_`0$+unr=<~twM3r zAPj^q74?n>h_18uH|2HB#wgdjCiSw{v^eZhuTD4qVfFR8 zkcWH%LMLx&x~?1E8jEHLlpqwiT_1roT?HRS@x|wPZx1Ct{J$H_8orX+s0bR#!JBJ< z)cs~pFN{NK7P{>OY#5(2772+JM3-UE-2n5)uZ|}m#vjnycK4LC z2o(R68unlQmr(NuId0N!JD3xr)wTWm`hI*&u`}JOGl5(q7ybw5k_)DV!HJ+gL8JYj z1RW`wl77Kw=At#+>IuW@SVJl3S5!4T4|!BhV9^rNZP+a%YC0q-z_D3{1#ZkIFhzAn zw#xCrbk>bWW^_WjWt0*Bl! zjBbqIK|Zj<$GDHSWA8l*orYl3>7iQo8)e*l7`}KANM_c=2#Wx$oIU&5Tu9DY?3? z$W*3c{(RDg$Rs4iEqI(L>vtTma&f=jcQ7Aat|UY=svdatJ|OoT&o55)ZGNA8a`JD( za7JP{>yNp7kh_Vb*Aqp{FY{xc9iVULri@(X#&+f!>ndi!Am)mJVn!k0^p1b6j!wAR zr1PhZ)ql%E^)C=|-^KDE1!Ch|3Q3vLH=ghYc-xvBtq+5VU%8k7$n@_WV}6=p;(64U zZMlM9`8%tFejUYNgY5kSEvGX=j8i|D_O;TKiqKd&w$+?T*Q~D_a`dT9fXx26-`ANY zX-=Z)2Oc2c`{)?vgFc%tK$@RT3mgQ0$1loP3YG_rL%^Lk;n!g~PFQN?!-=6p!jNUanH>v-12KAYXSxnY&Em=F@YOuiHEAq-?Ki7@^y7phFB)Uu?Xv2~N z@^q}5x0Al7XGV>6@TO!s5Vb!4%-uLPYy?`?Z-NH0vdG}X)762Dt%RKN7VgpEdG^6n zao7m58z9d^p7ZHUj?SR|?NKan*9@cy_=qV_k2C5XP4T($K08k%wT8dL?4eo#%b^CF zfoHI-$^XkjT!3s7{-S8=NXYyn_z;h)q{H`4T_cx2*?BFtl_3fmL;U{XE znuL`O8b)3(+DbKE<70b%C`YW)2V8RL#^F{7g$kQYG~M#nv75*)dWes`_OrHY9{ap$ zWK-gUP$upLc{^S8(XmEacXRE;H2p2-t6N)1k>{=p=fPFIwx z4u?{&VGr@e-Sxw*?DkdKL>vXb?kTvS91ZvUc3sgKbqz=0-j41qcD=N}5D)6Tan7DL z7r2immD4nsuBhA3Uf{GaWZYw?otPE3x(a+x)&9)z>5b?`ga55`o{!6}>Lz6JxN#}Q ziR=$*>7tex7eB$1$x3!#_I(<^qpU4<2;x97MSqa&5>nTif9qqRkwbr9MZ9?e6tBKK zn{?EjLVizt%w5Lx@Z52gIFVxwkDs_~uI>Q5z{9TCOd%kQce9tNZUuu5Vlz|fu;Ay5cK$P2t z`2GR&l|Xb1)EfAv1dc#Ab;dN{sMQM%PpbQu5-;|m;8AbuQse~5_UlTwcAr6@8z$*j z9P|$bhZDH%YSxNDc}+>BWNI*PFGqUtOXAz~BcZ&0i>Bjh+O7$u>Nwv<&=nUr7{?xX zK}u;+eOkp6bkl-Hpw6D!5cy1qObA!c=?ebvvuz%TE9=D7r%-DIjiskph2Bh#s`ATZ z+z+N<=e7IxVD>cFcJ{GvBgM|Q*DFoTWEni%oqT*}US@yLP|my{RdW89`9Yv{kUks< zf$jLG$%kyIG$qbx&wJ%tnOcZ%bx|<}Hjp=)?$L~ZeVSUFQmFref(z*G!wq0scA8A; zK5_pN<@zhC*LiwVq3TT^H9A))G1q}AVv~| zcT*OXcf&wE2X^kTG`LttT5-raIj5s61R|*3?O*wL?vQ(P2i&vanHVVUvo>{I4B!+@ zx&JA@TGVnu@G%A{ki`5P191fU;`=@t;3YgC(%;OR{sANQ~D=u@kP4=Jem6&!n2$xYjwdRH6^t2E72AM z`N&lV&|hWYs>r$Vebq8AU+Jdd`J_wgnt3yk4Pi7n{uXq5ZDxDZa8;$+7m?`-^z)>+boT2~mk$_BWD! zgg4UMVwytq=X+=(U87lOzt3pJ=kL?sX8`RXARZ(~iH^}_3r^#rRw2+v%0qd?8$PgW zY9qZrD0$7&9IP?T`dMOqyyMuSn*G+=viPjQ7JB)MOyLv;8qV%| z5}Zsy65geZK`w{YIhWr3C?o~Vk@#PiY7O^%a)a&nOC8qKOO8{pu?%atgK6={=f|?C z|6L>pfMx3u25BJmhg5UC;6-|hW-^HNB87Z^iDr5kOQ~Hr3^QOC1(*3R*wX)d>Az|z z5~Z`+{pCCjxMG;ED?XNW6Fl2KuKN5NVt-;xg^C3RfC2&}_%5;PnuIyGLkpOK{c+6V z*t*LwTt=!>D%jSnkmkta_D4T3psC=mUX~7v#qi1gk}3y*!AFOKMOOb3hE5Wfsnrp5tLX{J)DX(U-_ zIE!6A*K*`#R*|AkQ_6@(=sR~dL$&GYJ2`3C!#sixD3{f^jDvb~(47T&TemqnYh&m} zDXT^h3ZtPaiSO5-c@Of)7*3g6;+2kK=!$t}`A+!cj`B6P>zh<0Pr(p9m)#+p{ZTFm zz`VT@R|X&4?(s}ceI12jMLm^Tl!;TPv6(GXrRv<1xu`iR4km|gggB=)6-yZsqTIi| zi547VuG&e3sa#FHC5kHzbRbL+P>)53hGz}u*;?mtarriMD$yDqm0*fbnubr14Dnmj z@F)ZB?ax!vB`#W-7* z>!X>@5~_z`%|tM5+sc6c_{zwPk3S!0xjl|A;`xoMm)F#ZBp8XQ4p?hVe2U-A2nO1G ze_Psbcb881@uRanae^`4iM2^wDvp%A*X%979P`}R_bZ}D-m^R?j%g~RP{xvJxXzZ^ z1}|pYUf)`~yEl|1^`BDn%3C!N3?M6jScOq zG~(a>xlA35Ym?-K90*eEGFl4H7o`l&S0MuXg6{kAB~Uw1cOa(JN0YOR(a!0Ek=rS8 zx1>uN+&Sr)(3*tfB8$i@yKtnOsR$lT)Lfho&f#i-N$R1 z@iZeDrnX>=#nI&EP0dI1=Wkjx49UEtHdA;$DD#l7`0qh{*#Wpgv7A@1QT?H@fN)3^@hgvnfJ_ciU{F{EaU?bb^XXR~`i z5x9K$y)c-EOYZRsIqPb7B7r~Tm~^7*A?IBgnv`;ie1V9pCNGL#bYIS@Tn;vK1s$+5 z`ReTpj28H-I=;wTC%2kZOEi}sbM5FEPzgU`qFyHVi89qNyY$1C_MMDt7&Z~Fq{YSC z+pNzfti^N}E= z+*fkQYffwZX;z}PLo*a5F?$+yD87^*8n%WuIq~m`4@9>76-rF0N6X|Ab)H)9g?Zf% zsWkW%s?j9=?fMH)rT+gJIK7rGPB1v8aqoC~>%UqYw}$t0{|JbV{=e+>KctpnzA?Qr zEZdzDy^CI?XJ{8GZfdI^^%Nlr@NuQj*hvL=#sB}y3$6TNL{u;t`~Qcm?~H1y+q$L~ zl_n~kpa=n!j&uluG?Aj9fK=%yMIoU>C`yqoz4wlElqP`(r1vgeIs~MI8c6b;Jl=cX zAKxFw87GH~0ehXb=bCHox%&RkIDL$?hOTR?TN;i_k%OD%YnP!eS8IHLS1w_VD(x^X z-{P|I-bfLPM+^C~8HS#}uO`J7}}mWNZ5ZF$3)dCguzn4#Ts zm39s{VjX3&-JAPCNoBsvlvI+epq(19#j`&B+BN0=NLjlG9LNVoz# zy@(ZigCy`nwtcq-3ZRMxvRXV9=T7DYlNyJY8#D{w%pTy-_+wg+E5~B#$0LGaA#>(E z(oBlAH)e{lH;KEoo&=6XnZ*z@#F(Q;KPvO{@J4kg;O)>~HS>g*su$&%0_*a{U-3pb zzA|Q{lH>g!`C<(>2i#on6~NobhnF(Xy8b^S^xZ&dco|AHBWvQ~T?SVMOBtLe!^tmiTAy6%W3XeF>v!RwikCn;XCY|Jn`{Z>RpZbS@+!ku{9S?K1tK08 zgc^n$U-=2PZ_|$9+_d%J+%#guG*O&q{;pCd=$fdCXRPtW=8RWn}#$;1ncepP8 zhJ4#0EH8pUzK)`e$Cd4#G7uR9c4S47kg)~&V$;QOJQI;kBV<|};)3H`!G)Mp zHJN>p05S}b4Hok)Mm~F4g`I-V?tP={h|A)!Jr8y0osTS9jWTO>3WUU<)H^p}?Oe{1 zLL`=cPgQ1p*jNx|OAP|V3|@uSA?m22iVtDD2-;1p#KjT0-VXc&3Gn)MN9h;(s^z^rhTUDKjzP=dA;rVt-)7bdTNb_IXBXs zK3r$*qi2T&{vnXDzYXu@A(4nz{cBww`~6jAwr+rTsu!PmZuhl(34%N`o3IMTBNA{ViGv;4lif11*?ZGt|M@!e z->gF+$>4fi;@k7EIk=!{om!KFUQ~LLW1@<8o`BB?+Rw`r``)Jxkzr|0eHy)LPv1k| zb9M*})sELCqY>C*TEecWW-zN9%m{@+4qyo_>oeu*$GiGI|anJ`RUuck0`p3Yle$Ik0uz%P!4V5 zr$Ev3e01JaMUiAFEY_*(Mkl+ByT8sRtQbGYo2N;VJxSLWnI*x=gZkKRfcM>$;>3>U z-45pEEsmVZE*c?Avd-J2;|dKLQR%*9M6is+!;+gxu|ZbJpr?qs_i?)Mk24fmAdSwnyANHrXKH5QG}giAc%S*oWR-dx#i!?+S*bJ`HuYru ze>*|QvuHG?=zr4*EZvjqajebV><==kt$|Sm7+^36fHu}~ZNKNDaaWF`y@2@oqlAmo zmqqNP6EgbCKj6sZ>o{3zvIhUTfH+y}WrB{ZL0G_Cbfh%NieD=FqrAWgCEzA2_dbV4 zfPpaigFn!41p)r`^88~HnjcTCho4waUCB+2_!3PjK%8Oyr`3Bxc23a z$#>|DyQLlbiXV$}y}OiZ>u5&9;4x2GcdXX0kWMaa!wNiDUgi9L%Nl5pd(0PLjz_wp zaGb2EtShVN>#?npi*7wWjAi5tv;@??T%#WA@u}n_i`1=HPR>Z0zb~fub|YRf^9~bv z5%cct(k#d>E}0)-W)$mjls{7}3-Epln(Ky_II;U=%%a?Czd(z_iI{@J=bI4upA_Dl z_g?_!3rme3Coc32ep$nd98(^2zbD;u`-A5fzdqvZR>RkGvEe{9Hx^ThQT=Z5F_5u> z9a!^sz$h1Wjz=mI_wc3y(M}2<`$o2U8g{ks3r}PN$aUkKX#xJwv_(<{yWTIbKE8t} zypCw_YwM0IdF+rS)q$1=X7J(xqyUjM$+YG3qq&Cxu@Yb9;md`O2S^>aJ~Ya$Zl8Vl zC{9tK+IT_Wx_A{z*%{A54a{lgm0H$LHAi>d-8X-|c6l|hgb~Ba|8zQ#f5{y3EN@=T z-NPr7uM4Ta?;c-As^IH>M{+3q^!%j@hrzfQ5D&_Q8>9_G{PHpIba7ByUDqY81g)O zjWis#{_zg6-z0mv^ls4X+jh@D%i5+$QMERJxlFzdGdR=oHf7TrRenhj_Z`ZMM)Dy% z9hNvW&uuPur^0;qXVs|A4e&8A*x@%gRo zmX;!KEHT~n`yy6zQ##)_Uu;|Lf6kZzMS2#PjA1>oN7%?uuh3b?2#2`T6ARZ)dSQptQBsZnR0RFp;=LPuW=`;Qy`4hIhEm@rLVd48?PylA>0ZRi@z=RqX+!} zS$S%sEn0Skja!1xKmMj_VG{OCTYY265ME3wg0ms<6h}srgIZn%4R+8VPAt8mTTPP) zKZ&ipVrUSpvdeGJXq;D2i{+Dq*UvLbqYm#}ruN5Voy3u$$k~kN9kQApgWV2d-bgnM z+OnhcweRI3pGw)U?kv|ngw7SQIdr3y`4-Edng^rV%$yaIL>ca<{q~!}Sqk))344!k1NVP`OHRKkG!0m49ezuw&yox+R&Sz@7VVJ(N73tMs6Q zg15dIt_~nD9>tIX#~p-S3VdAX?<$6vl61vhVGG^cioi@53}p5b?dLO46QN=OO#G8; zE~zeKJ+a(mWx+Sj#aB`(AABqS^3FdvVsA(oT|VwcTgPtUki8@xS`7h4Dy>oGQ%-5J zGtFhJ|9*_;E;6hiWNvv$C}@d*u9oT7pOqpg3PsChEOJRibE#KeB~sTm_xt{mpCs$a zOM9Q4vib9ktDcu+#lSC_J3{IVX1hfg)K$r5?`0AejB)PDdcav$!vq(_^<0JjTE{NT z6v>tiT#2IgU;E)-7X7I#L%6;zEw1CBdcyygrQMa_0{0>0C+-d|PdfN~X$2mR6Gp&` zAB5ueKHEP2_4f`x_WL>L%y(7(Hm+Z3#)UoZjbs zaHX!e?>RFGd zFoUs#$_{Z6n2r`%(>#0rXCzjcrbYA)4fa=S&F%7#0SC|m z$uZ;gt)_-QKMzX$3=WI<+!H`@6|xc=g6&Y$WtWMk6B%=42bJqxC0Pl;aMl;qW02I} z!x_$cYP;CTJ~8Ze#Wj-%qf>Hy&n{<7o!EV+{S>48-0u-VXB}J8ocx)&Nv-eFgDU~m zea@wm9fT*1MsJ$7PgO*RTJE=%+hC65iaX(v0xe=bc6VQ2KjO=Ko`&%FwPNsyRHuVG z?PWeNU0cOrOYuiIYj^9cl~0E~P8RN`aM;nD-pAC?vPOUq6iE6(+5_z=?l_oCi-nG?Htp4G18D}TNOYAMW{yv^=+{xwSn z`a{G!`>v$8Y*-gEa-~nHNIyFQ;1K1hXzxdX1GlG?q|RlD?6*3%ss${noZ>G^c^i*N zpu;YObey)drP~LWHT->QO73!1;cU=3tV2I68P=}huOj6liv1v6!RKDV$8QIQEIwTJuAk;%Hm#nZ(URQKv?F`In+6*UgX9mRRl z5c!6XQ{K&Rp@y+)wRb<%UF)QLdg!Y}=RT?YdR9ce<~V-#_}Ox%LzY;(qaw2*$^w^! zKu#{XLP)jeeR9F+FC#vaIZe^XeJM@9-yf-bg9;q=w6y$*+Ql6&b)!kTu&)I&P|t){ z&f;5JR{s>ixyE0!Y%yIfFxS!XKS@6wMkSpIVrVdgz5)xS(!s|FxmFy3rjT0?E48*pv%!+-~MGkEgL0?pFveKC!4V)q`MKTTB8F z0%jQb11F)qD4gz~f)Xw8{@(ve9@Nc=U3weo_q+z%gI01@oic;oz?cdZ^`YNIFh5H6 zQX52bB5C9pytqA7JrqN0@lUOGhl6|+v^#U6Ug7p7Ut?IHqdA#7WArsE$hPed&&f6- z#TvwjxlL50?1>KjMCLpXyhyh4BbPY8Y0Z&yZXOx#54>y3cWm!g=IDw-63{TTpQ$Xm zErT_eTN@yKzw1q*Z{lRnWyj(>Y-PS_JWC<=L>YF;jzGSeuVzKvS_M)RU`klETv$QCuY5geZ(Y~ zeKVouR*Wc!bVj6d-G&*>R;8Wkavb9>w}@ddJNn|yanbVMMH4rp39P*Z+}+Bov^V}$YPRlmF$?b=V8yV%$`nNM%k_{7%UXz z;|@hdXV+hdS&!jtu7DUhlvs5JhLYdunrUlT&R-b-AP{5O%1QP6To66vVG8`^_454z zyCjX#_y8&VlMz75C^KU8CB_8CeHO*qTn(Q0YgEJeul$bRtt1tA^7QW`T14|q@PE}t z-<8tW;|t8?k_L7~qm=lz-|i~9%PgngL()RvJq#*rY(5oHOS~&dt`JF`$f*)V92~lVC0f>{ zH73YL{!f^BDoXQ5hxP-6(&5e3!r}$&tg&S;Zwq> z=5$83_#9%UvBVD%zGL5ttO_zDK-KMEI!Wxd>M+0Vh>tp^EgI_PYqX!OoCajoA)PKq z9G7HVUyv>NV8$*NFIz8ZqN$T0=N%B8QK3rehSIXo#~Scb5xz>T5)YnNPmizt%oABV z%}r1V>|DMT{Yb4SzIIUT)5e45YHecu=6sB&&(YP1QT*BP4^@11!36IrPCeAOe6j&b zfu9(=Jj)Ome9)ct<-&Gj$&&vIP!9l@(FU_%sd7d01D|W~{~KSK>lv4>8Lzu(YvR`CBAkiXMdA2fS=x z9Fh~i?}ly6`J9*;L}8DU1Xcfe;us~-zj0tEHS3Vr!TB?njh!{j?8Q@1$!?4WnmT$U zdgvQW@_3x}N-Hw!cI~9zgdyoEqB(Qq^QvnP4vND_j~Ggv=wo-xJf9}OQv|A@vINTm z1n&a1IcYNHDk~j2{lULExpl}uBKfgtf*!m@IoU=@U&_iaMgvUevMyAWD%V9r=`mfu z8mXg3fjOuCU7rMywNw_Un?|O+*AD1?DxJvfdWMo8BlB-GuQ`HJ_wtZYK@Xta*Y0n9 zrqxyN-h%#05AOCX^0ho@j;Lcr$N7u?15ZM%6_dZ}_1jLk}aKeG+@^DCx; zPPVrx7Lh}`Z>Q5UPmPJumd|;@AnT>3R4Rdw6#ifv|Ln|;T$rg}*^NHCB3joqW zp5f9C!K=>rNE&6c0rLEJ8*+R-l5kQa{X}sv@%Fex6wkk?3vRd+;m3dFax9ZOe4aHy zc8D0IkShqq3IE;Qt@3;JL2{;DKwMb@fduKo^(52cFDx-vbI01t9eK_iL3If{zk=w+xvP+bHC4r(IB=hr`8Mpm)&-1L^znOfSt6k4S9968- zn1H>)nS1>twc^lOs{TGfVBo?5Ip*{bf z;|xaaZOTW5&&`GI3F}L|K-QRUeb>kWufN{sSy8C5;VKhmvov&p=S(iPz9L z!AK?976~(@NsmF%NoXCJ#7&=X`-(}A5vAP(&pb>7$5ciw`e7d&E)R@?MK-G~P3?p}Dv~^ZcgTBsq<_T?P|Y!m6n376fbwgw185m+ zh#$?4?@04aoYED|A;M!s$M}Jxui|Iin~1y%%mW1OC+;RbaBOvv} zV6nZn-gy|9VY2Y*TL#E?x;fEk9}W+YB829G4s3tN zK{er=S%CHgUAD^O0$7+xqZss6&Y(ScOAe=ZmcM;&1vU zYqp?woQ{|I3qDiV0T&lw((X|Y{c!uqN1Fw6q_H59Q@p|{v3kf*utgM6Q|60wU3e(* zJpp#_ES;3aN8r|L28oVw?I0cdB}p--0qcY2!+2wa*jS>gQZkJpk@SM(PzNy;Q`?uB z5Rj>zPe28Wr^i?A-WsWKehE}gkU|PlkHj~-ANG%YQ+ZY$VY-0QqR7)%%s;-VrY_6c zTlLb%;0kJMYrVG(w#?sgnNuk>)rJFek;CPPllxo3J!l!EGy0=O zoWo`<5KOfU#z@Io!^~uSiP;07(rD&~0=~zry7}dhDZg>53CFk#_l+g5n<6;Bp=ppm z_a>G+yC5LG{y~vKvOhnedrk93N?W?WNIT2z7~N&fH!;v^jm_;Exdj1`_gSUD+!;K3 zjR1bFlGku=xtDAEdS~|XUvrLenIUmljo6M^S3Zk~t)ie1KUf47; zYwhYURcU4>KFv-Wih7b1^$J3l9hV_VUQEvco+eW$@=sIU9iU{ai!uTE59KVWR)pv5 zypnmp<9B1H{KS99te~Q0=fWoocgQvp+zke^e#L%%Zms+&OVmG3hNxphMwIp@^LCl_ z@@z{Q*m^6#<{5`);7Pkuc1v10%ycj>$YqOUtjERk_`ajjRJ0tiBrl-hV<40xnI>*J zd115gO*;kFOnpGhQ_F9+_BH6(Sfbw`2vQ zFY83%G%IF7RLx?eWh5c%%Iuw3&no+|42mQc>g@>holX)FINHkUR|7s20PlHoP|+U?U>%LW*@O&-I z4Ym<;H){CRTvNm2rwyqUA3HcYTVJbErT({KTCg}tdi3Vy(~rl9x?V=*rsneJ_gACY zsZ$Fe^NI;Yg1zT+{GyrZUN%$F1CUZ6!gi?f`3WI{>jTK!N_Np&EQo%e&KfIa^IXn& zYW^>*~uT=*yonzlS-Ke`}2#1EG;z?S3CVTwd0AqcH*gGG( zlEK1*)rRkRHIz3h;Xju@Dt9Y_&e?>Zx%T36|A9q39XgLAc3LQKEayyAV z>Xm*?lmqxK3oOE$ON}81v3!V~{*zS7APE+DYsjIsKfwDsK#0(bs7nv4wmdMXBzQX+ z^?JXjR~YFp8BOSX8jB1eawb}Pr~@|Fc3WYU_I4kVx5221@lL`CWhb>-;MQis$5K%5 z7rvvw{CAL3u(!Ol^qSoXqM|=rgIKun5iwt*V_-l`K5rgjHr@GKV`_-?NX3{Yr%DL1 zeU;-3IZ~wxGqmTJJ9la#;ZYUs;H&w$`mz5s^r+D2j&FYZR!)b3TvE7RyVoG3TWE$F z&Ffl~zL)Cv$QfN5Wy?AIdh6P1d56RkVx7ju6`(q|U#da~CB=PjfraTE^&BX7XjsN^ z4@eU#+#9xD0Fo4Q%Luaffgrk(ravZ_tP+{KR^YOq9@YXP>+cBPeYh`s_|WpQ zhj#wXHyosNyd^qt=Sq!-b;Oh6?nj|(CKnaC#n~Z!U&c)J?uGjA8&e*SR94;>E>3eW zq7^*M%U1fAmR!f=d8^^#uoA1$-6O(wKoJDg-Gba-Q{Xn(1teV}b<(Pq`3tPPsM&CqQcNrGNWc$j8c#(=;pDJqL@yIJ zpIqnmq?R_M>n>4>2fgALKvg%}1V~8Pn8QqXeSAn9zJb4(f^uU+po^FFJgPaan- zBc3l`op2|pW|>znRmf-w=OZ_G%Hb_}psGAV?xEh!^Pqny-3J!ZDViKtXFyF&l5i78 zX1;NP>8XJIR8mD=!t{R9(1YgmdDGE_sPo!ErHE&fv7)EQ~=+9tT9z9qANu*Gh3s?ox}`s@AK^4atQ{g_|!!s}7$m!|O#n~+wX zeJ1X9@j=5uHYTo(_TL`w)_rlmqjRuL#wQ1F+=BiLv*+R4C(BDu<5Ec!dsArU$l6EY zWNm>_vgia?@LWv{GUXPKaF}1ASZM5mQIm%vX z_L-J_tS?AMux*RyT|i``EyHh!4P6g^-XV|m+UV#vQpq)k7P4r{-!E` z@h*oja>yKSl>i$bo;?K`M5(f6&OHE)s!es>5N?ZM!*wJJzU)-JqOn95+byvlBC1+n zd5_%x8+YTgFHfN&*rM~SE%t5v7v&6Z_TY5XT1RoF6jr@DgP{JgKc7}fI(h53%(dL6 zf$SjT@?|kou!zVh^E$jvZce}!!0&qITA*fZn3?wz8CRD9ub{_LO&KQA`afFs1C;=< zN{T`3R-Rq+*Q+PEseaOMBbh{%(gFoEJH{53+zclH@TIfl&M!~J?ySx|DuT7dp)tfC5c8o;=*T~$xux7A=F%#FD zoD&&(=xDF2GrGg>?rzlR&WUDmpbzlMab|w-GylUAlmr0vMB+RZg@Tt0`ZADp4 z0g$RZd@tmU$po!yHSLC_Ied!R6I|9yKH57e90_zsIs1!AXf@g=7C0o<3P?TP4|}Phlt(>Cx~H_IYDl_ActXzN4%281$7WQ6(TYk8Fz64STED*Dxe7;y)pO`xmuF$#)c3h@I*DTX_nW%W6ayFMs|7R%;AKwaV@UIR zQ-Zb5Ozb4iMTf_5GO$as>>8hEc7_NN3{psgMR--t5SUDHr{-Sp922=x6L6ak4c1q_ z*1Fc&`zM{qKFUN|Le)}#h_m-qYO>{2BY_j&tCSbB^>NzudW6!X9WShlnQz}JvFtk; zZqqG}SMrv>SZdJU$-l&X2SRQ-gcY(fV5?PjW)dLax0Kw1V zW~@KH{M&^+)0U04B~F3M#Q5U(g0O9!=hC3G*p6CyqumLnLBUB zxGrk)>)I4x;lII1hHN%r#`HWlLxx0)5oQ<8b-_OU5o;QJ=V`IasZoV#oQHZ0;KFD7 z&e@-ob>vwpQQ-;#h8k&i?xpJnlj@DX`O03`0xulu=FSx3_%*RuTqFAB#Z(9Zcoc?d4XX@6!?E-uTa(%e{PD<`;$vd{plkY$1 z*1d|OcVxZgt<98yG7f?d5#1~2L)=@B(Esjj>k=vFS@O>E>ECGSK2+;UC_@OPCMdEi z`c43Q@y$g3!|HRkTEAMijTQoe&+;4AL~sG}-QyBNX4UUX^fjNQ()QdPOW)pqaIE!Z zwlc1^{iUrC&$-=T8Vm^5_Gky(NZ} zM#V|DK2pc%Rt=7IRPQ}4TP0cUtw2)UFL>&hers`REgB{rtQFoN&%< zVm^KD()Rr%X~O=CP~IkN=8rG(WFP}>)K~Ju`FTNwXe=mIrKJ{QyzVGDN#vi&mTi;S_59H={v8?e=}(N8Bp#IC%fgXsjF+yy8VA4p%#XE*vY$A0 z29F9aNv;pkcWegd4oF<%iFN5~(1JDw6wl4m9vCLB4*afV^Z1%21`R|md)?FxerUaQ z_f&b&Yv93HX$bm-`wnH+o;f|+(6Ilr*l;Z=Xp&Fg8Vex@={2Cxf^TJG%_mV zQlm)6fl~ZIo^hScJf{`usoQ-IO_~t}Xv(ydIL#h2aeF|E3oBY)np2s4o*LtK`43#u z_!lhESlUIW|Lx)SJ1c36PD>TkT&CFA{-@f*7*)4EFoak4>{36;vV;zzMZ;_hO}5mY~X45sz=TzdA+OkdjXhT^!eN#|wp$a)K0F zBG*6AfhH}=J4-21ulL!czZcDZ)jb|pWj7hhBp3)Q_s=G!nEaD|T=Ffz)NqBz-OJQ% zJHNd2k>+d`1Iq8u@}Bpg09zdnZnk5m$Zl-(^Ob6J(^M_K z-pQ+A`}3*mgtUw+(U#TO;9`nyv)i=+LyqU8)TCsOl55>D@rP(_6GD$yFc#UT8O1qU z2hS!E@f)T*W!VF^-q@c%*qp^daTw=O($4$lk`m`VEE=}+na&mM<98L6s zG{n{y=!~gCJkLXY8#C{Hb=(w_?YS9~w#RVz%g6j&EAYX_XQOCmpDA@N%%|t}9vlAU z>n2*yl2Is4X-x1)+bK2~74+6K%NpOPi+*z%EMCm@qBCP)QonyH!Q8wqn-fAu3l$oeJ2W!>bwRvzVc`(LJ^WLOPX>y}I!qy2we z=*diW?}qdbD{N;vA8L32vFUk&_Kz_L883%;=x?}{P6fpLm#M;cz_Jc;Csv<9bt||C zoCJ3N9Ovzqz622yw*n9r@w>Nvi=0)35%`$%yH(0|DyViP*mAFh?<5MJ@y4$*58l(R zz^`K;6YqdN;YvQpdE=ZhO)pj({S&h@p+3@Vj2zFROc^F#WNStl?0Xa~hz(1bX0)t0 z79=c*SLuSu8K{Id{{%@O>qy)-WRpY`(q_m<46}!3Q*<9_XZ1 zS&pkjiwLsouNSqY?v5E|Yi-}u%GrCQ6EBaAIZK{y#HQA~=zB~Har{2@Bekwy<-&Fj z_bj8z)5YZ|zVu*P)q00Te>Pju;=LUK$7Ws418!=jRPV&~jX~`JQW_|V*RUO+KRqbo zf60ppxsrWJRn31Xan;JBnmA+M_b^0`e0s8`vqDgsCnOkiVOrkdrf2NaITMgIgN`@u zkFCElMx=xsi;_L?P$_&)QTf0^YCOC7hTGWugt69aKi`!1M5BX zBjl0GWSowzjjT*?!jVlek&RDLvR7N3^48QNy_bR15Qdss_I|@)7r3FyNou!(rpNN8 zV$oh4c!tac@!reJOJC~F)}BM-%vAs*_idCJb6FhsV@5jlrYt_uA5T()?|jRvOt*?J zr%q%{A%>AvYjcizxMr`9VKE2m7yXyh7-iU#7Vh!OFaOF_KwaU7raas3L~py6@IAE` zG_m+T&JL~NkrieXRf30F18DDov}DNFfE3-SB}4eMl{8im7FXZ(8CU5YdJZbg6v2j^ zgBS&GdkB{H@7fZ*{kxme;%mu0cYFp+I?q~xTMRMcjINxskXQ+~ne=Ih9E#kycg?Ii zm&k-KE~ojwUI0-hqQ3&-@}`Fgpl{Cx7e4u?C)GGEa?Hn02kT50RM2jhDpOBU9H0N$KTmRh-TU*2okX#QKb%TLf-buo5V* zZAhO7FNgdQk>FY1sui#RNl>%-pv- zv(Si)U0|!Hd2xhdh+dbRs0L8uDEu0hgCVDU!HRKNls~ahGa12m?xT;^dA^lK$SZ{C zJ96;Qz66Q&d5soQ*Ic`gIJ%v4UZS?mWWAO0muWcs18@z3*=fsT0$cd5uFchNCPqu} z^R6YQXmtOhM-<_?0}u}(Jc*?{96+Q+Ev+RliepDnFt3W={j0CRlb@C45IlNMvsWBU zJEp@GF$1)zA4ivK?1{U?lc}ZpwWz<*#3-~1@7vFkCBJqEnY>PiOUwLp4(gV+Xkj@C zd7D&eI=fA()P#Oo6DFDBHwkIz$vQqJ7;-vl^Q(yd!}C~3hmZg-M( zmj{7pu$|N0n`$)Ds+ZaP$|8{%-c7pkq|84Z`1$>4i@0H%bGqzWhdh*MRr|svzMys2 zuh;n_0arbj4Zl6r21!fr>)_QXDP}eyZbg=YX!fR7R%9})Ev=hnug7!X1Jbus+TQK= zF6Wfg+l)Gev@KdYR4AKtK{)54R1I61buBY3{loH-q2F9rx4$)pZxUkzzU5X!_p@{o z003bB-kzjXow<#3p=En{fnuaRkBOEN(H{1+!eu13THfNgU4z4!eK*dnjoatZ08iok zX--hMD<Z z9lUY$BX#pz^(QAINjeixmaNZw>KDe8MPasvOCS6BZQ0|c9DiK|pO5)i9? zexDK$cQAbVX1z80gVa~6?RvkRKPFkIc&BFK}Y<>`y(I`8$7oa^vOIj`lKZKS_&Mm8F9zcnv=6N ziAi`;eo&1GpOEkspUu)u`VD4|6?g>hIdP|l0N;`5&HuIyng3539)4J<~;xNhvpuu9kdC{nlM)cuNoy_L1U$2@S z{v_`4%0OLOnvMu$Zf7l?pIE!24#%!*?=|tV(YjeC4UvVFs#w=8jbHy&_Gn-i7x#?K zx?Ksgv=8iakEy?d)N$zd58j2%S5Ee*c7mf5{b=$xeXGd z#_uIiSy*KX>!jGy8{6x5c}Ac$a_^DYQMU%9khAGIOc8=Sk*efWDvLe`1o`(1eVpze zjZoI!koCF1-dw&5nRE9YdGlkWxHcgE!1YZQqdbw%RvoonPXnvzQLPD-wm~ou85A)- zrNJ)$y*+G<1c@WPySPs@s8uhC{y0=t8oK@_rwOvPQ?~{7^b9XDAh#^I`{1!^&&#G) zk6^26($Bb`e8vE(K|~8Yk26I&heB-g0K(%7_ZDb~eLD z6B|yc@%7t=ExNoTY;vN(OXh!Dfslx>iDQ>e+`n6&m*8Lc5A2O^75|OBhk*QyPjx<7 zybD(t9Ez6&o-eg>V4RnODq3+7bg+3(Dh5t|W*Hk(4+%|5qQxhsY5kK!Y5ttRae%IrdQzIv%GqlfNXtIg1fqZtSXG1z&MK&NFkIN z8YEutLOUm}F*5)XDJIVs6l=f1!DQl&iFcy~ETEFX!Q(n%tp5CTL*OMrx=5z{cZ#KzMRI^#I(IQ6~WWuG|y08&Sa20bWk=iVEBa7 zch3Kyma2mGs^Z2`1&KXhm+~ytMqij-fNTOZkAr3qcGuaEOW)|=^6ouH8EvpLTh!OA zmc6`=Qy=Yn3BJe>tu}5KlCiD4fzwe9g{V4I=hUYgH9hC-@hzP+`!^V2b2IXubJJ1j z&CQSXhSj`Sl!V!?&E2lN{gPZoLZ40J<#mC7tV?G1EYwWU0sb=nFW*3>Bu15!9{u#2 zg#Z}|G5Wj^r~WUF7rf4*egCca2hXXP3vxKquRN6vGp|?ICh}}M;|2yGqPVG5o#>w) zJXdzcf7Jie`NOvWc~oEGBTN`Ibd~n1I6)O|ocZXl${x5Y@(y2E0wUC|o!RhRyw))c zfc0Q>(D~8?SvDUt^9N5ZI(#O}GQz4>eL|rb5OgSzkc#Zu57)xvk214iWPrbCaS59g z=`Kl|XGMk-P~LHDzG{Y<^2l3H519l>Yu7&j6KF@7LMDZgiz(xP#Y;0d<3Y2xMHbC& zkZCE-TCZ{L7{bAIw-#yRn8r&O1wqh`xB!#;WE@%#-XWYW7R=z<0Ybk~L+SE1z8!SP zG3;DxtsVcx59QLQjnV&nDRnzV=&?0IoJ!wn zG7op!+G40U62zyq^0j*pB#=aw-Ty=)cQh%#Muy5l$*mT+MwPp2>ZHzJ;MfXeNM-4+R(rY8^_}`W zcJ)Ye&|f*WEFzfzRMSZ^wh^LYLgA{Evf(v!Moc9&&Eo6N!>^qp$Q2&#QTS%*%-(1% z<6C43&`q>9{zgM}+0)@HQ&9_h9CAwIwBWU~n3t#dBjPeKC)sFJS)2T|Fd` zfKWhe%Ju&m?CuMpGk&)D!=q$Q9yI z!uRZKwJczOx|p5d^6K_&k#5r}{pj14d7ee!KBnb;zzyT#P;!J^BAs_?0LSgK1z4T5 zg4TO$0yfGzsBb+dRiq#pGlM;9`YUgAM0*B?OB@)o0wt0)K5=}kSj5i{R{;ztcHSt*q=w2AKQ2waS+H%+6#()sH%!K9tv$ z1=}Ls(fms!IqJ()YW4h%Ojkx+%v&$>$#}<4+baU3)@4BgxzcwjQAx#^+X}`BFPoaw zVg-B{g1Ao44A6B5_Rn4eM_22ZzwDUy-%7C$f2g4xVj%MME*`cUYEKgbC>^hd=>(dF zY2KaZASNbR+noznOd=x=DgEg)E)9GiYrAhvk85tefsVrW4kuk>mT*Uxwtl!tp6$ko zWO+O#5#FS#^3>)zo=`jZ^apNtpo^euVS1D`H8(0_xx+>mb7X8u=|3n<#AaULwkoUp z(1I;vvDx)Ax6cc<`U=h|LQf~s3E48vX+qa80JN8J&0J_PbQ2(eAIV>92y=3~dcyx{ zL@h}FANjg#BZVh4jc9HCO3vYEs4==9el^&S5xf$i;5stezuG6=xGIr$GkO2iahbPm z0~R0!I_G1n^vliQgOz)+RSFPn7Dm4oJSY?&CLZxLP_3%vv?Z2~KF_l!$oxl(kaqnX zYOY6Kqy6dEulOaLSJ6nRqVFxx_p!#|*ErF=p5D24mhak|VLglY5(KADF7`HXvA7m&h^m3hW{~=LA8K7SzZG+LXE{a|> z5Fpv;yw+V8v_Zo9Ib`Ko*EQMFWLZ0{(pRSsf_wBny;#&vs5Rnlj9Xn#aQ5qW$#Pp+ zYs~0yo*qi4nRZI+aU|)J_1Z$$G`cxTjmGYzMJlAv_QoL%WSoISOXPkhhJJ**yYl)B zCB-cTu=&tzj;;zdfj8gqJY_b|BTL(%(j^1CM=!ZnFnI=WU9*6fgf4z*0#VvUHvYHE znmEa7RhPpEAgTkAj65Y)qrIHkH)!lFDzFx(3Tnr+PWm?9U7hVlCQ%k+Q_9RNB*&7@ zY4PnEnafQn)O&8$-$H!*=UOj%JIJH^i@zfy(%04LtB}%)O4Poct|^X;8leN@WFH}h z3XN3l;UDZO8Xj!TNc?^p3vL+8H&I7sTJyNB|2~QQN8^3to>JO)&p$xaJ!@J#Xs$Sj z$+Yev&b+TuiB#JayJ{8WdeGLd%v93~p8GCfL5}nfSw^SxL{iT|Q~FCl+gaACTGzI9 zwm}mxURA{pFvhjp1RwP(BRo-|_xX2dxZb3!{E2?q%_q#oE&qjJOs9G$3OJ3IRqrQ% zbT!J~sJUbPzY|>%h!nWpk)9w?Z3%S>+`hr8CA~1?Z7#E7xU{bozRuJv=_0Oam07!8 zgB|sb!}Di`@bWy$4o7?edccZ*2Dm3Ddm3RcOOpbhK7J0GylLsR&HZ}yKdLVTZtbk{ z4{t4Y%KvwLV(NV?c|Fh-mkVc%3&k@cc-A9ivk{lmmLgZ0ge(`9e@K(S%&=;-AI}?8 z@F&D_VmM)jvU8hrYFJS6MFyLc>a*8rBNfS)_hgAX*2|?d& zm#gI@xFoX~iHD=@R>&rCq=2 zx_|1j+@Z<6uGbiR!XC2~Ynf%xDE%P>IUMPB%i4XSSxYN3KG%8DI_9o$q`FY?7g?zO zm}e<@Ml#Ds5c_^SC7Y=Q>Ptv>o9Y_i!8WA)-oem;HruGk6L4PmdY!$mdQ;5PgUu+o zrP}-HtEz`g=RB$m&|^AQBk=kL>;gDfhN`{cWpnwB7&lIqEN_?`8RMqJvH5vG80xY7 zXhA~#hTW)y+9Is#hIP;EAJ(|wXtMk7JAkhETpjh6Yg;SgVSf{Kr$8;g1Yh;uuRESZ zEhHAm+ySUvA@Nup71wk~;L)!_Yg-nwPxrJhhV3<0yytsg>W;V`yeKoAVhPE=!6Kw{ z{RWIFt7C0T?9}@IF?HVIRR7`sKL|-iA=yeuB{IS>j!{Xnlf*GIGRy2(hg6ci=W&Sa zEqffY9WydI_BiH2IJR?s?^B=e_xJs$ORkf;y6)@sdfoT)`Fz}nTbKB!az+H`RRW7H zRu)PAXEpo=bPNTTGTwJa+|%CZ1e~HI-lyJ-1hHT2i#8za)h{Ybjw;3K` zXHGmb^(3oqu@ODxxs%{)0bQpRGhw9kXorf9?~P#^-J>(DzFDbE_HP2lN$`BOj=*vr z%WJO)hFcm~KQQNHWKtgyk<` zkbKL*qbrJntASjSv~77@lAf~P$efgc!%LP-Ugo854wqCg;2N7K+r5y;G_^8?P!nQ6 zt?8&%TV|DyZ`H;6g1_V&vWNtc8#nE2(Y77EXDwccxcX=uBI72y1xFjj!}kleM*CCA zP1daKJ^R+j5@M)JUMKYRxqOAnleu#Ly0otPgokx|SNjwsC*#6pCI~2|%_(&k+JxQ+ zG;7SJf+Q!RD(>uFRVnH;Waj_!?1NuN-$BwEF_+bUJSK)|t4k(Ogs$s`$1H|l>nVTB z`Xx?1tDes^Swi9nvKcQSzM9bQj(v4~)M0?ul#aG)o3hp5(@1o0rAP}uP8_@Eb1p%t zTC=li*}HP%*H!4?(`qLR3=b1>{!;B44a!l*F=boM(f??I>b4u@9o41uiPGKJ6RJDn z3AH+N3n2lW=?}-yVHYAen&k!^T&bijEU^I*4eRq_7*%SI^b>{8G~0=fWMB@LA{UfJ zBn}-GZbdkL`Sz})XQFAV^>jr=gjE7ljzdl%wl-yQVhXN@vu>Dce(=R%C88}*jNsF4 zWs$eteGL}rW4ZNgzn;Z6JzS5uAE+QDj~IB#ctVQcH3+a^587YPlVX(rg>Oi+5QB~< zbde&wuDEm|MZxAia`(%$BelQE)>z5kI??tDX$#i7U1+PtZ>DaHD$S_P8-hKm^f$X(&++S4V zf7qO4WyDb5R!t5uYg|-g8njA90F}{G7JQd_rh1D7EVuH2p*+hGDR%e0RTqY;0SJ-O z^tW10>5KOC+WclWT76OzozA(`_Kb09QRneehEB)QliRh=uYk?5o_t2(Vs0+AbLmI# zTrok`^5e<)X3LWm_q0UOU5fYz5l4kgCd0vHTFv3;!N3MfpxfZGSl4$sdq1bjb1L37 z>@uFXr8+UMI%aHLhd=us{v2hn)p+XzMS?o=aZ5%481yrJ;vq`goJ=Gxh(Ro4F)P7|gQMSKa7ad$f(gcZO1<3;cEj6TFFw2_6xGO$?Cz zfTc#~cY%J@@-M;@3HGUe$pm9mk*cECF9(^QN2Q*Nkt#QI&-M- zl5sXOAlCIM8=1+^ra_fNJe#P@?r*Awm51EF_2oFN2J=|K!5K2+ckhM0K8ZErIbGq0 z?6wS8`NncAFN@h$sXNF&iNAAvesuln@7}FrAo7|K)$xf!!mZb;$~;Z-(P7eiG*n2c zn4`O6&^%K!CI~sc znS38%ZB2Ma!vzNU?PuvsZVW0KLN_U)8v{urtK(WH%ST406|3L!4lISW?*c(%p0uD# zr?$`Tlk)qaDEVblJQdhBn`Ty5zu}~d+*GcY24r|(in3>XeI4bSqmx%@9*aKpo`Hq? z#oo`41fF+*`_Y=978FS`>&sCar#X!AzD*iw{|DDN0QaSuBzr+d$(O{44I{{!*ZKC{ zK>z&rXJ!qP_yAgIf$WN%ZLSZ*^M*-3D(NU^S$80gSpCwi!C^LhG~JEoF<^1CLePF;cWOZ=J^wQ}G>K(`T zJkI-B)u6)>%kh$hk(pEMQd5EK+k9{Y3Ri?)aE8~^F@U2)h{r+kB}Xm9(i5iq0k$Y!(h)nNgJL+7YCq7NQBXy4%Nd&{a% z#c);f@S?WFRoyIo?z6!@^)yz=rzQO}&YnDqWO*x(1yt2TojCgYOP zMHD7IQ#IQ)Exvkf+b=#P`iF9a$dzLOFwp&7UVGo-2o&S1z6ht~;G(ge>-viEW+poZ zuu_JwS(ihawn1#&jAqVJl;9lNd9y0+ZZea8U_n@a{7ZRETh4dfxxv!R# z@#+94xA{J^t*Bn{kC3%j*g4R(x7-_!mC-=)Z6SltTr1&1c2Lch^CW5u4jDu0Yz^OU zs*uVr3BGr8wXA$PoMfc!J*|I!=S*QmZ#|auR@;nQlPl>z4lE0d#2cJQ)JY!Kb{)6k zfZvdY##U#m(`6?;q=whxejMIrKxiHJcVJwIsvs1bM171a2KWW=?yW6*WT~ma!Ey9k zaY9g<-td)k19{+n(r9`4;MhAtYCd=hH{%YJRlq2HszaBd8JetolC19BAkAkce_)z+ zYev=ua32Adpvx6}ITisjLJPmw67dy+=diDqHQa1H2Xt?bjJAJV{3Pr#=+kaA-7raf zFT&v**Q2^tR^6zf*k7$9n~}5kA#Zk|X-yw(fWk^@a3E3JTr*9^sla_sQ(Eqy;H9!Y zzPBy{)hvcGZU3>AQ2^Rou-)0=p*D=G?5>%h>|1W#Y=kt!c$psHHkpcf0Rqn6LN4iv zJSIdhi(M1%g8fr?DUnc+dZH)exzO}A|KxQsq^7!zPf)c09&}eR>|Amft#PVlpg5Vh zGM@k-D%qM1TQ~D9i8EB2$#(6jWPNzQ+SFH@Dp42gI0K7%UiW&N@$*m+r+JlUW$%9P zvYb!WkYl64F7{s2KwVLY94pwCXC`xfz2QrKFRZg``hN5F)0PXli6~x!eyQ^jN1ruR zOLjG5JWDUac5BJHFapP*M;1KnEvFKo*TCWUs{aJrM++I_yc5Jl4s$T)fUOi$!^W5t zOrJdW3h!G6M|=KuSDb#mQ8Rgei2B{|zEJhF=QMa~T7{(uWF545k#SuYgmnM@;ieO_ z?%+rXOIZJV;a@Z=`KZBY$s(9giI6J)KD&C&;%cbHOi}z_(`1B6-buu!;J?{%%pa*; z25asAl}~%a$_Z=Fb6H-EBA045Y`%8(q^TC53IQcvPT1A#+uyW+y%syk^>(_Qp=mK= z)bUsQVj#`#{)%A#Yp^ZM#Edl~AKd05Jqf*pkCQ$VgSo$sMVZzRq)g5)z(EG!4bLuS zTQ&dwj<-K8{H48F`?xyu$LJBiqnOxL^C{*>+!dNrfH2&You?+5>>P?;bor<+C#5;X@Cmtkii@b`ryEP>m6Vev8EGC+>OJqxT_ z%6j$LYmfa0)KnR9ThB|&_=KBt^FDt>y=+00tD&ze%Wm2eC*|OdL`Acag$Q&n>Km=A zK1;j*t=i@k#M;+O&2yJ7)<~4Bwhb0`!d|7He4UgVoK-=+&T{0rlNw=_lWOLD#=g0} z;bPI#8x%^he;&)P#h#J{v@1K~p0Y1WEDz7e2LfGfl{sRNt2i5Y4h~T=;NbNqjJ1Kc z=T31J%ymDXI|X7rKRy+7mSGQgQX_UZ#j$l=u4+~)a>%6(z!k;o=uo~JbmnOD=Peot zBjX{1-NguoU370&Sgqym((%wY$@)nlLMF3&^~35_j@Z7cZawOm1l*VJqZHg@-8&Gz zkz;u`X$PZ8YhT(Pu;E*q*;{;126KL0L9JaoG$3aQleivO3<2MXB&#+p){Yx0V|?if z2TQSB)J@>Kd@|)h>M&r$IzBa``0?bIN6B}^&q{tP@@bMzzRI<~7q5G$yu+Z9H6ic* z3S^0W;ZYcyeJcQKk$y>^nr21P!l>=XNM~HnaRK$uLR81N;m~7or&nkCOu>Z!()=Co zU+BYtl4pt=BLJVGD<`BoUtM@KYT9tc?_N2fP&E_DkQP}8!Dr`Q`!AS~_W0GAWJmws zf6sae`{N3NMFX&o{CNH7JAqtoLY#x_Ql(M0pf4qMq~80QvF_Qz0cJH3?;5T@R-9~+ z%lB)-JO(Pg{?45XwP`svaBluwFU+H-Kec{oPag1xPjnWxwmL;W6#%(K6B11JY!XUN z%6N+S`t_48*l77$935?)a-cUy^)jU{8A5weJ6oq1bO>r!mfGMzM(d0~tUjHeId#IN z35jMm$H}=I5Q0SvHQ_{tFCK5+T^qFGsc3>Jb{5iR!F;as3f4PwyN-ATg4b=`17zo{ z4*D}GfPC@dUU6!WuZj28xEkFnYBM4H_4nGH#S!eg1lHtT89xG99QkK?1({lY+UY2! z!Rw~C?^TjLP)h@aDTdY9O22W>n!9$tKm6*EVZ>2nMME@8A^NmJt_~`Q2|$|34;Hn` znJrn%$Lc*Qm3xh@BaEa-lr1fLCt1z$z36ahGusDH6xJu#4VtUgKZL?Q7Y*ZY2NdvJ zX?o(x&F}g#Lw2hi!}hIrsY&Rt=Ba^;qnxcc4{WS&KUKA#Zc!ra6?@pDg^6&-AL{;^ ziK|%mq=4uuwGDJRNSy0V5Oj=!U2HsHb5_C%TPEj0vt5%aw}h?iXBzT*iD90h_^hX` ziML@E8bJur)x@B(bXIS!x(v~`(PEmuWafS!pk|`4OB`4okDNy>+8gUrodvDE22$## zTC@yT;PVIoegTkZ)_8`A`_j6|e;k??i61JtVdrP6KJ*dk1!!}oRMJ8u z)n=iZ)jvz{KTkP?UW9@+gw$Px{xy0NvH@-z#+w-5qwwrhf z@S$wApeDJAtzxYNR#2*BsqGo;TKc!yI7*7B)|bRi{@))x=3W_S0DejT)tU%HNC{MW z!m0u&Nnq5%S)2 z$IJGp)7fWuhp0e!flF1p&1q}Xw-8h;(mB?YAFhvDf6$q4PYoF1nY4475vt|z??ac? z7Aw_la3ey9O7)yYGC&J-Xub)a_@>uL@5=OW4>EXu6Mi$>7kQgV%<#_!IplE3v<#S2 za&qXM+tFllCHpRc4VO5jGVM>-dKV=q?KrhaQ)MGR*a?mI-5F_3z?;qB19{}l7CABD zyQ7w1Aj47t-Rs@6;N;nU8!!~u>C!gR?YQ9W@8r9kqL8EEdJz@4uH>g#-(YbLDOAJR z{_OliC&i!rw+)xt03brBb8G{CsAlEb)~Dy?RU*gyg(%fTD%zHHv@Tk=9=L(-QdmX@Pa!hfRhpP6R4&JN<&E9A} z(aQm{T1HaRCWp78p19K-PfDrjKM7@BnvHqm{)XT1`k8BF%@>^;_%T_2vLB=n42f3- zZz*9R>9>tOoPiYT-6R&$H|Jd9y6aHzzifjH34Vk#{Y*xwuviF;%ju(MXpkEr};ae%w-+dM- zco@fZBkz=f`X?K{M;KB!3yCgDn*alvKD7X8nde*}8#0-RrQH{-M7KR~x`uzOdnovI z0C?hKMRQ7d1lP8Km_BPIKl5F6yzkvnBHdy``X(~cu1c_yEnHVnnn7{sg}lIMmVC>n zAQy|fJ?F9O#&BhETc$4o$SxhM=sWN|K&8RvV@GsW4Y_jD zVLyp+!O+^997^0^#fMVNVp%rZ*I_d?R*|lJzv-iu;gPz-{zY9`wM7pe02(Q1lX5&|9JsqW)88~0&C&hKuh*l+CjGv!&jdcYQ zGDso$ntFe9WcFcg;UF|p@S^Sza4w1}erCPE$+pW}^x%mAFrQv{TOcS$GPdkN_98M% zaK$b@*?`C3Z`G}F|8;Y^uJoeVI4cOb-+&&ZP=sXP1ON#nZ^{>SJCp@v$ zB!rAstGEfSn;I5*G@WeLBcs1KJBQr<%$S*zXt1-<=Lhdb5MgO3ndy2rRxFW0#R}9<%t(LqY zc19D0cAOCd*nSds-4Y`b*JG#*Hj=K^s@?uD6Sv)mDRpmImtb*cue-g*!3$ZJN{mv2Xq^LKuHw~7_TNm> z)tC@|IMn?+Em?q?;S2tmoOEjmvT*%y^u@Jb#Z;zRz=M9?nwzr*gHDZ6;Vf35K- zLuGO^UkgVxKInIV!u9nM6Z8G#iiA`|w&T0!UB6UEk>4MD^m8gF{OxT?U2y-gQRXX> zi{0i-9vfn5jlg^U^|T)gtvgEsG>?}MyJAKlixzP2`rn9-=-a%YLe27FIgY{Z+Qb_f z$P`j#AN|0iQtfRt;6zG&_Voes7dv*K-^|bR?th|S%exl@m6~X0+7^&?OQQws#=B2D zb1Q&*Z!k~HV}5cKT!fIRR9}AnIzYw~m>{&T4?%x_ z91eI}+;J-4v*+n&bM$MBK#ksC;9)(|mVHZ@-C+@dCtWx}I2T7f-E|?z(u$dt~OWru|`J2eO7oNeBki0e8UhSsN`a+z7JL6CL&+~w3!cm;JqRDV6 zxc266$2yNhdF00LO@U|s$R|;=kyo7`LCaM!h>SS~hD}y!Kj^VBpGoC- z?_x@rw`N24OD#T0!9LNWMjwANhOL~NiN!IQ{!*Ft;M)CGpO$SHek`r=v^jiTdAi&O zww*cP()A`vwI3XNN5*22q?+`p`xXLOxO)2j%pgVXTI57p+VqrEoBdN32^ub4uGI>G zmHDr`^Qh>xD=#VA*Fg4TeP7k6ANqh!V>jBbk;_Z#cQdy4Ut8k;Pa~0n5l4%^oqXDZ zWGaoA??6bCLw0et7*c5UFQrgA;uo7}kYIlJHIhq))Mot8Z9M#pk8gpmwh?z6&P3i@dD-dLzwjamRJe%?X7U6 z0K1cy^;G>v=*hZ}pE8f{j)omDC+acPlE2>^#874JT1-s34(u2Z7l^h;(X6N!yYd}1 z^vlu2!GR{%BS9igEBt8SI%AHHqpae38qI&M;mX-8n(imUn-oAQ^#wV=d(w` ze9pcBUgbc_pNom^9xHMdL7M*29Aq-L8PfGwW!=fiDVv4-1>~rKGt!TiQzGt)Xz0O~ zVx-9LFB&InS8O5rWdUX*`2JC%YCdbT;jaTVp(BnuuS*kV%B3*lNiQkoK-PP~@udYwV7+M}n{g}9J!?^oNZs3Vq&cZ@`2U9YQ% zqO5Y6@UHTOt-ziJMkXJT6@g|gqx1oH-SU-fGgh|sk2YBN$?EF90+_6dTcEkdOKkR0 z;5Q+??LN1%3x+GKFDh8YpH4r?1&azAb(}b>EI@ zOWG~}8Bo`x-tqh-hHCPiIeR8bj<_M~wlqUt3|!=AgL^&85EdisS496wuu&SbrRSzo zh6p9WUlGt1=|Oz|X3-w`KtkwF#XvO2|)cukfEymB$O95C28k6hb=g0D0{+QQI#-U~M3;GiJyS zGb>`(n`Ko_*f5lI{NwWiPX2T6xTHD7^5?+N8qj2093TBK8l^|jO#&Ix=4hzwL|n|( zZ2>bCpooC*&rgUF#r42$@GjqzeY-~9B~C5`$ z8($eu(mU*@a=NgsMx^$Su{M^HXWN1t$-)WW^c-Esraa4Nmpxa=(Nx7@s@<}B%Hq)O zd@B76PKCf*87<3kD62u6IInR^mSK=`l;BYpo?l%A+6f{%xeL&xtv> zUb9#DIsS31?y+pMmj5SBOwcEK+jD!pV-7svmVLE)&8#wK=ytT<@7W9T!hsiN%XAOB zGDqkfl2^ksT2-b4_IxOcQ{9si647*$7|-NKN(r#w?t0^HJ8b}0f+%>s{*}|O)9G4|8y1P}Sl>29FO6NVF?KhB>(o|Rm#kS9(TYaOd^Vet#PkT2f+yHE5 zpi*0*@ZeG zqfxja(faIv?(YV`Ad`Hv{lB3jam=pXk6(W)aRx&2;8U``SMIyQ+zI#+G4C#tdT@Xm zS83xgU=Od0NMF7)_BYKPF0{n@l~~mLae?4Ty?mSSSzi$1e&J!>)P0r+!lE(^l*n1q zG~D4viDiBhb2{-H&`%r+PAL@u{a3Cs3W^)-E?XzLyF9d365aRWHmTmczY^z`F`Dl% zZgb{*V!9g$sX0wbK&AGCRvSe9gdb`VS9c)3MEi5!o<5)XVzE^o z3~v4sdnrBeA)m7!e@M{PN$!L|{H4sGL=eNyiWIO=L_G-&wVusdru>k3o1wBR21_kiXTHYFd!tC9W`>4eoeHK$Q-iOs1WXeWFZ@g+uSr^`go08!dEG>kEsKY8p#ZMaGl!VPUy`z?Wn>gJv; z1@LOL+3Gc{2i7Ob$$FRbm7@$arz&4l)?TaI}DrEMNfZ-Z#B>3It z;+1!g1X${>OOUs8_~;E5(k64*7oV3%IHH2W(DgqmIK2Qe(E9vU!*%!HW;&P$B_vo} znhW{sb@8bKtej6*as)WaJ8l;)fGO%=?myQRv5h-96C`V?v%LFOZ#BrN! zAgZ{zomZPaSt!z|i#3S}Ed4SRk_xK3W>od7Wzg#SPqmx#5iA){v3Z|^%L!k!fvES3 zf7kLJnzak{73`>Jpkz1IofNJf3ZYb5fe*Tog4yBg)G1E&SkQFgq?dXzv?5@tb? z{{9NYt1KeaUO@o;)nX41X?Fr#C zLWVM5jxjN2;WzD|&^I8f;`K+SR9$eU>d;L}e^io1e+C}+qxmc}m){9RNT&n2awAjU zd_VT2zx04ip8bs)_eF$c3O(H&o_k~kRORNEeFrRmd+#&EkxRnjFYOx{)+iau&m9)Bn*1!T-~CIT+kFv#Rl!Zqn$0beq}>!J z;N4ao70lmd`~R2#HS`Zy-FNUU7G=rkZGjoQ$>y`4ayn1Ju5sLuTdba=8^zr{$bHFz@amxboz>sTY+^> zczClSQgwcRb=P3VtJ7_!GQTzWKwn|~fNzZz%J!>+R@mgt&O&DN*Js>NS4*kJ3n!M0 z7AOvhfAG~~p1Oy1BYP-2n&*prJ%@T)59NV0dMESs-H2Y)J7KTO$wz_rY-#v|#E1KCW!X`G;Ok52 zhR2zmeRe?Q?VPCk5xbA;y*~jzX`mEXgwVJj`#-=HEs{jI_i+PGuCaQ;dczeDVbP~# z)h|BhuX(ml9l2}#UBQ}56no?%01Wfw$ehVHpIyuaa!Jnay+D|wco+!k?xtvUWrsT_ z+}WvI&vDzj%RYI;YO=c}V*01YAe5ya>}}|`6)YG}+>KI=FY)U;2ze30n^ES0&V;W! z3-g%UXyQs?W32E$J0q$JU!h`^nc4nHW5mrFDB%d=wY8Ih^imDHM;R`6S=x1XP@NLn zAi$f`oWfaQa&NXz!R1VQR>MCP1k5$BTLPzKKOpu{Y%q5f0JM&g?$Zj7;jG_Z2QtQX zM~wHhc2209wH>&*xDPeDW_n2ksWv4lO1o???g2*GoHR`VR=9B>2s~|y`J;gI+xx%f9Qe6UE30&<2Z+A zVv3|Lef;!IZA)aAtb786f1AKqAqQ;u@$ieT`WCE9R7ir$*bJS6KRkht zsGO?-)`jQ1MoKo-EEq={99uktO^+`9hXm%CqL_Mm%!7z|_u+q700$YF!~X{noM$<5 zQwjdKMgsY2D4Arb=9WDn>n2qn%d59}s9f$W1rn`596)XUjgkL1huq-=6LMa@>iBm< zlQ`6*tNJf(da}Plo$&o|X7{Ak_2h)MwP<;!F)8Z&8r;6~DT@hcevxN&p0a@^QO~TG z{Otw*JlCQ1&+fR6+69uYnyneoiMOi2PQKNXmlJpAu^MDj-xzpp0v?dLmkTJEn(T$m z)SMO&r%?r9931}J@6-}K;D@PZ!38)II+jd{Oi33nh&2!kvmB;wLdz}Th6)u&=0I*jw z=d~Jl|I}@AEP#~-OZleNt^5wtGwV~Ckb6)<-dVWoZP>mjC@MH0%ydfd!Qvqt-aom4 zv=nEN6K_uRM-2+o&ZkM^W~x6kTkdkT_KT$`%EWz`X^8Dx>Uj>j^Tm(+7eB9RGA*n} zL(k<~-|JnKK-QBgH7MZFQGY$g0tQ=~pp6pH!W1RLz+=*UCQ->vvHm4}7~dd^W)sDKDwl!#0!kATOx5+*;&im?xw6>FQHTVZIx9N z0Uj4nRQ)-4K3q+Y0e-1UOU7$ZeG2hOv+?sB|M-20pXU-%#`cG-{45U7R|ROAj%RTi zR2}kvWe8`UyPs0jsQ2a=oEa(-0QBmwW zrjFKQfLq(~YW(SV((VwTKVugtFBU-+WL`oC zg+^kCJ*`U77rLeeK_JjhcKa4dEwdk6>$GRB zTgWIVP?6$O9q9qid~_8eZ#?MzMQLunVfQ1ym+%(Wu?GDWct_S*S+H64rIdad?zRzZ z?ExF>{os}k8An6(CCgU4cKNF=rbu9?N{j$+U}38eBQd14v;|g9vIia&B zrM|YY69NszF~AZb&12otdG$7-K;;i}6)wx>fM=Q;cK2Ow)^PR!-4}%)J~7*uKHD?` zgLXG`GO__5S>=oF-G_?wq_?X7hV6o@+7Tx;!x_UFym4Iy<`G@S!*rt+6)GGkc!>9A4wItsyl?MKgVlE- zpDL~6#lJ4FyS4Y<{5)h%9`<4UKww@thQ(SUTT80L{8DItvy5sp3mvOuaOr3Nsbz1} z!5Nn5ZAUyXniu27Qih?1X~}6D_wdll+(}F5Hc-NMf+s#K(~VXVTd%g zVb%0l8?hNl@CVa22$cc=(UWN4F`$Up81J?jO#mi8-XP_-O}CnJb!F<03rq%^W|zja z=y3M)Gv1xVscAyKL2&g@S}sCH#g-&Q**2a;4}R~k=XC*%T*h{0C29e1qUO5qS@0-^|6C8YT|x( zmmHUa5br;~CbazgP(`j#6zg>+wK+mVv^`}q-CiV`2sLR_UzG$0pcsV|)z70x!tmQd zDqYtC!fNCXj*#M(U`y&yu*CSz;k}8VGmf?u`mfVLXNMoAlCTkkQfJKJ!%uFDdD7M03L7&x z_Y_~htWBRlks)`ZF3dpU7bvgR�{id?`)D{)MhuX{9QL&%Vcx=@cEcHz&I$n{Mk$ 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

B= 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�r)3GwoskaMp5rsXJd5p~+}V|GuE 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⪙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